lol

Published on May 2016 | Categories: Documents | Downloads: 135 | Comments: 0 | Views: 1367
of 337
Download PDF   Embed   Report

Comments

Content

Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

Il manuale
della crittografia

Guida completa

Nella collana Guida completa:
3ds Max per l’architettura (III ed.), di Fabio D’Agnano
Access 2010, di Piriou e Tripolini
Android: guida per lo sviluppatore, di Massimo Carli
AutoCAD 2012, di Santapaga e Trasi
Autodesk Revit Architecture 2011, di Minato e Nale
CSS (III ed.), di Gianluca Troiani
Cloud Computing per applicazioni web, di Jeff Barr
Computer Forensics (II ed.), di Ghirardini e Faggioli
Costruire applicazioni con Access 2010, di Mike Davis
ePub: per autori, redattori, grafici, di Brivio e Trezzi
Excel 2010, di Mike Davis
Flash CS5, di Feo e Rotondo
Fotoelaborazione: creatività e tecnica, di Eismann e Duggan
Fotografia digitale (II ed.), di Paolo Poli
Fotografia RAW con Photoshop (II ed.), di Volker Gilbert
Grafica 3D con Blender, di Francesco Siddi
Hacker 6.0, di McClure, Scambray, Kurtz
HTML5 e CSS3, di Gabriele Gigliotti
Il manuale del Guild Leader, di Scott F. Andrews
Il manuale di Arduino, di Maik Schmidt
JavaScript, di Yank e Adams
jQuery, di Castledine e Sharkie
L’arte dell’hacking (II ed.), di Jon Erickson
L’arte della fotografia digitale in bianconero, di Marco Fodde
L’hacker della porta accanto, di Johnny Long
Legge 2.0, di Elvira Berlingieri
Linux Server per l’amministratore di rete (IV ed.), di Silvio Umberto Zanzi
Linux Ubuntu (III ed.), di Hill, Bacon, Krstić, Murphy, Jesse, Savage, Burger
Mac OS X Snow Leopard, di Accomazzi e Bragagnolo
Manuale di grafica e stampa, di Mariuccia Teroni
Manuale di redazione, di Mariuccia Teroni
Manuale per giovani band, di Pier Calderan
MySQL 5, di Michael Kofler
Photoshop CS5, di Edimatica
Rhinoceros per professionisti, di Daniele Nale
S.E.O., ottimizzazione web per motori di ricerca, di Davide Vasta
Social Network, di Marco Massarotto
SQL: quello che i libri non dicono, di Bill Karwin
Storyboarding: dalla parola all’animazione, di Marco Feo
Sviluppare applicazioni con iPhone SDK, di Dudney e Adamson
Sviluppare applicazioni con Objective-C e Cocoa, di Tim Isted
Sviluppare applicazioni per Android, di Massimo Carli
Sviluppare applicazioni per iPad, di Steinberg e Freeman
Sviluppare applicazioni web con Django, di Marco Beri
Sviluppare applicazioni web con PHP e MySQL, di Mark Wandschneider
Sviluppare siti con gli standard web, di Zeldman e Marcotte
Tecniche di registrazione (II ed.), di B. Bartlett e J. Bartlett
Windows 7, di Riccardo Meggiato
Web Analytics, di Davide Vasta

Niels Ferguson
Bruce Schneier
Tadayoshi Kohno

Il manuale
della crittografia
Applicazioni pratiche
dei protocolli crittografici

Il manuale della crittografia
Autori:
Niels Ferguson, Bruce Schneier, Tadayoshi Kohno
Original english language title “Cryptography Engineering:
Design Principles and Practical Applications”, ISBN
9780470474242. Copyright © 2010 by Niels Ferguson,
Bruce Schneier and Tadayoshi Kohno. Published by Wiley
Publishing, Inc., Indianapolis, Indiana. All Rights Reserved.
This translation published under licence with the original
publisher John Wiley & Sons, Inc.

Copyright © 2011 – APOGEO s.r.l.
Socio Unico Giangiacomo Feltrinelli Editore s.r.l.

Via Natale Battaglia 12 – 20127 Milano (Italy)
Telefono: 02289981 – Fax: 0226116334
Email [email protected]
Sito web www.apogeonline.com
ISBN: 978-88-503-1325-9
Tutti i diritti sono riservati a norma di legge e a norma
delle convenzioni internazionali. Nessuna parte di questo libro
può essere riprodotta con sistemi elettronici, meccanici o altri,
senza l’autorizzazione scritta dell’Editore.
Nomi e marchi citati nel testo sono generalmente depositati o
registrati dalle rispettive case produttrici.
Le riproduzioni a uso differente da quello personale potranno
avvenire, per un numero di pagine non superiore al 15% del
presente volume, solo a seguito di specifica autorizzazione
rilasciata da AIDRO, c.so di Porta Romana n. 108, 20122 Milano,
telefono 02 89280804, telefax 02 892864 e-mail [email protected]

Impaginazione:
Infostudio Srl – via Perosi 10,
Monza
Copertina e progetto grafico:
Enrico Marcandalli
Editor:
Fabio Brivio
Redazione:
Federica Dardi
Traduzione:
Giovanni Fulco,
Stefano Marconi

Immagine di copertina
“Disco di Festo” –
http://it.wikipedia.org/wiki/
Disco_di_Festo.

Licenza Creative Commons
BY-SA 3.0 –
http://creativecommons.org/
licenses/by-sa/3.0/deed.it.

Indice generale

Nota dell’editore......................................................................................xiii
Prefazione alla prima edizione................................................................xv
Ringraziamenti per la prima edizione.................................................xviii
Prefazione alla nuova edizione..............................................................xix
Ringraziamenti per la nuova edizione..................................................xxii
Gli autori

.........................................................................................xxiii

Parte I

Introduzione.................................................1

Capitolo 1

Il contesto della crittografia...............................................3
1.1
1.2
1.3
1.4

Il ruolo della crittografia............................................................................ 4
La proprietà dell’elemento più debole........................................................ 5
Un ambiente ostile.................................................................................... 6
Paranoia professionale................................................................................ 7
1.4.1 I vantaggi...................................................................................... 8
1.4.2 Parlare degli attacchi..................................................................... 8
1.5 Il modello delle minacce............................................................................ 9
1.6 La crittografia non è la soluzione............................................................. 11
1.7 La crittografia è molto difficile................................................................. 11
1.8 La crittografia è la parte facile.................................................................. 12
1.9 Attacchi generici...................................................................................... 12
1.10 La sicurezza e altri criteri di progettazione.............................................. 13
1.10.1 Sicurezza e prestazioni............................................................... 13
1.10.2 Sicurezza e funzionalità............................................................. 15
1.10.3 Sicurezza e sistemi in evoluzione............................................... 15
1.11 Letture consigliate.................................................................................. 16
1.12 Esercizi per la paranoia professionale...................................................... 16
1.12.1 Esercizi su eventi dell’attualità.................................................... 17
1.12.2 Esercizi sulle revisioni della sicurezza......................................... 17
1.13 Esercizi generali..................................................................................... 18

Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

vi  Il manuale della crittografia

Capitolo 2

Introduzione alla crittografia..........................................21
2.1 Cifratura.................................................................................................. 21
2.1.1 Il principio di Kerckhoff............................................................. 22
2.2 Autenticazione......................................................................................... 23
2.3 Cifratura a chiave pubblica....................................................................... 25
2.4 Firme digitali........................................................................................... 26
2.5 Infrastruttura a chiave pubblica: PKI......................................................... 27
2.6 Gli attacchi.............................................................................................. 28
2.6.1 Il modello con solo testo cifrato.................................................. 28
2.6.2 Il modello con testo in chiaro noto............................................. 29
2.6.3 Il modello con testo in chiaro scelto............................................ 29
2.6.4 Il modello con testo cifrato scelto................................................ 30
2.6.5 L’obiettivo dell’attacco discriminante........................................... 30
2.6.6 Altri tipi di attacchi..................................................................... 31
2.7 Entriamo nei dettagli............................................................................... 31
2.7.1 Attacchi del compleanno............................................................. 31
2.7.2 Attacchi meet-in-the-middle....................................................... 32
2.8 Livelli di sicurezza.................................................................................... 33
2.9 Prestazioni............................................................................................... 34
2.10 Complessità........................................................................................... 34
2.11 Esercizi.................................................................................................. 35

Parte II

Sicurezza del messaggio............................37

Capitolo 3

Cifrari a blocchi.................................................................39
3.1
3.2
3.3
3.4

Che cos’è un cifrario a blocchi?............................................................... 39
Tipi di attacchi......................................................................................... 40
Il cifrario a blocchi ideale......................................................................... 41
Definizione di sicurezza dei cifrari a blocchi............................................ 42
3.4.1 Parità di una permutazione......................................................... 44
3.5 Cifrari a blocchi reali............................................................................... 46
3.5.1 DES............................................................................................ 46
3.5.2 AES............................................................................................ 49
3.5.3 Serpent....................................................................................... 51
3.5.4 Twofish....................................................................................... 52
3.5.5 Altri finalisti per AES................................................................... 53
3.5.6 Quale cifrario a blocchi scegliere?............................................... 53
3.5.7 Che lunghezza della chiave usare?................................................ 54
3.6 Esercizi.................................................................................................... 55

Capitolo 4

Le modalità dei cifrari a blocchi.......................................57
4.1 Riempimento.......................................................................................... 58
4.2 ECB........................................................................................................ 59
4.3 CBC....................................................................................................... 59
4.3.1 IV fisso....................................................................................... 60
4.3.2 IV a contatore............................................................................. 60
4.3.3 IV casuale................................................................................... 60
4.3.4 IV generati da nonce................................................................... 61
4.4 OFB........................................................................................................ 62
4.5 CTR....................................................................................................... 63
4.6 Cifratura e autenticazione insieme........................................................... 64
4.7 Quale modalità utilizzare?........................................................................ 64

Indice generale   vii
4.8 Fuga di informazioni............................................................................... 65
4.8.1 Probabilità di una collisione........................................................ 66
4.8.2 Come gestire le fughe di informazioni........................................ 67
4.8.3 La matematica com’è trattata in questo libro................................ 68
4.9 Esercizi.................................................................................................... 68

Capitolo 5

Le funzioni di hash............................................................71
5.1 Sicurezza delle funzioni di hash................................................................ 72
5.2 Funzioni di hash reali............................................................................... 74
5.2.1 Una funzione di hash semplice ma non sicura............................. 74
5.2.2 MD5.......................................................................................... 75
5.2.3 SHA-1........................................................................................ 75
5.2.4 SHA-224, SHA-256, SHA-384 e SHA-512................................. 76
5.3 Vulnerabilità delle funzioni di hash........................................................... 77
5.3.1 Estensione della lunghezza.......................................................... 77
5.3.2 Collisione dei messaggi parziali................................................... 77
5.4 Rimediare ai punti deboli........................................................................ 78
5.4.1 Verso un rimedio a breve termine................................................ 78
5.4.2 Un rimedio a breve termine più efficiente.................................. 79
5.4.3 Un altro rimedio......................................................................... 80
5.5 Quale funzione di hash scegliere?............................................................. 81
5.6 Esercizi.................................................................................................... 81

Capitolo 6

I codici di autenticazione dei messaggi..........................83
6.1
6.2
6.3
6.4
6.5
6.6
6.7
6.8

Capitolo 7

Azione di un MAC.................................................................................. 83
Il MAC ideale e la sicurezza del MAC..................................................... 84
CBC-MAC e CMAC.............................................................................. 84
HMAC................................................................................................... 87
GMAC.................................................................................................... 87
Quale MAC scegliere?............................................................................. 88
Utilizzo di un MAC................................................................................ 89
Esercizi.................................................................................................... 90

Il canale sicuro...................................................................93
7.1 Proprietà di un canale sicuro.................................................................... 93
7.1.1 I ruoli......................................................................................... 93
7.1.2 La chiave..................................................................................... 94
7.1.3 Sequenza o flusso ....................................................................... 94
7.1.4 Proprietà di sicurezza.................................................................. 95
7.2 Ordine di autenticazione e cifratura......................................................... 96
7.3 Progettazione di un canale sicuro: panoramica.......................................... 98
7.3.1 Numerazione dei messaggi.......................................................... 98
7.3.2 Autenticazione............................................................................ 99
7.3.3 Cifratura..................................................................................... 99
7.3.4 Formato del frame.................................................................... 100
7.4 Dettagli di progettazione........................................................................ 100
7.4.1 Inizializzazione.......................................................................... 101
7.4.2 Invio di un messaggio............................................................... 102
7.4.3 Ricezione di un messaggio........................................................ 103
7.4.4 Ordinamento dei messaggi........................................................ 104
7.5 Alternative............................................................................................. 105
7.6 Esercizi.................................................................................................. 106

viii  Il manuale della crittografia

Capitolo 8

Aspetti relativi all’implementazione (I)....................... 107
8.1 Creazione di programmi corretti............................................................ 108
8.1.1 Le specifiche............................................................................. 109
8.1.2 Test e correzioni........................................................................ 110
8.1.3 Atteggiamento tollerante........................................................... 110
8.1.4 Come procedere?...................................................................... 111
8.2 Creazione di software sicuro.................................................................. 111
8.3 Mantenere i segreti................................................................................ 112
8.3.1 Cancellazione dello stato........................................................... 112
8.3.2 File di swapping........................................................................ 114
8.3.3 Memoria cache......................................................................... 115
8.3.4 Mantenimento dei dati in memoria........................................... 116
8.3.5 Accessi indesiderati.................................................................... 118
8.3.6 Integrità dei dati....................................................................... 119
8.3.7 Che cosa fare............................................................................ 119
8.4 Qualità del codice................................................................................. 120
8.4.1 Semplicità................................................................................. 120
8.4.2 Modularità................................................................................ 120
8.4.3 Asserzioni.................................................................................. 121
8.4.4 Buffer overflow......................................................................... 122
8.4.5 I test......................................................................................... 122
8.5 Attacchi a canale laterale........................................................................ 123
8.6 Conclusioni e altre letture...................................................................... 124
8.7 Esercizi.................................................................................................. 124

Parte III
Capitolo 9

Negoziazione della chiave.......................125
Generazione di dati casuali........................................... 127
9.1 Casualità nei dati del mondo reale.......................................................... 128
9.1.1 Problemi nell’utilizzo di dati casuali reali................................... 129
9.1.2 Dati pseudocasuali..................................................................... 129
9.1.3 Dati casuali reali e PRNG......................................................... 130
9.2 Modelli di attacco per un PRNG.......................................................... 130
9.3 Fortuna................................................................................................. 131
9.4 Il generatore.......................................................................................... 132
9.4.1 Inizializzazione.......................................................................... 133
9.4.2 Nuovo seme............................................................................. 134
9.4.3 Generazione di blocchi............................................................. 134
9.4.4 Generazione di dati casuali........................................................ 135
9.4.5 Velocità del generatore............................................................... 136
9.5 L’accumulatore...................................................................................... 136
9.5.1 Sorgenti di entropia.................................................................. 136
9.5.2 Pool.......................................................................................... 137
9.5.3 Considerazioni sull’implementazione........................................ 139
9.5.4 Inizializzazione.......................................................................... 141
9.5.5 Come ottenere dati casuali........................................................ 141
9.5.6 Aggiunta di un evento............................................................... 142
9.6 Gestione del file di seme........................................................................ 143
9.6.1 Scrittura di file di seme............................................................. 144
9.6.2 Aggiornamento di file di seme.................................................. 144
9.6.3 Quando leggere e scrivere sul file di seme................................. 144

Indice generale   ix
9.6.4 Backup e macchine virtuali....................................................... 145
9.6.5 Atomicità degli aggiornamenti del file system............................ 145
9.6.6 Il primo avvio........................................................................... 146
9.7 Scelta di elementi casuali....................................................................... 147
9.8 Esercizi.................................................................................................. 148

Capitolo 10 I numeri primi................................................................. 149
10.1 Divisibilità e numeri primi................................................................... 149
10.2 Generazione di numeri primi piccoli................................................... 151
10.3 Calcolo modulare con numeri primi.................................................... 153
10.3.1 Addizione e sottrazione........................................................... 154
10.3.2 Moltiplicazione....................................................................... 154
10.3.3 Gruppi e campi finiti.............................................................. 154
10.3.4 L’algoritmo per il massimo comun divisore.............................. 156
10.3.5 L’algoritmo di Euclide esteso................................................... 156
10.3.6 Calcolo modulo 2................................................................... 158
10.4 Numeri primi grandi........................................................................... 158
10.4.1 Test di primalità....................................................................... 160
10.4.2 Calcolo di potenze.................................................................. 163
10.5 Esercizi................................................................................................ 164

Capitolo 11 Diffie-Hellman................................................................ 165
11.1 I gruppi............................................................................................... 166
11.2 Il protocollo DH originale................................................................... 167
11.3 L’attacco man-in-the-middle................................................................ 168
11.4 Trappole.............................................................................................. 169
11.5 Numeri primi sicuri............................................................................ 170
11.6 Utilizzo di un sottogruppo più piccolo................................................ 171
11.7 La dimensione di p.............................................................................. 171
11.8 Regole pratiche................................................................................... 173
11.9 Che cosa può andare male?.................................................................. 174
11.10 Esercizi.............................................................................................. 176

Capitolo 12 RSA.................................................................................. 177
12.1 Introduzione........................................................................................ 177
12.2 Il teorema cinese del resto.................................................................... 178
12.2.1 La formula di Garner.............................................................. 178
12.2.2 Generalizzazioni...................................................................... 179
12.2.3 Impieghi................................................................................. 180
12.2.4 Conclusione............................................................................ 181
12.3 La moltiplicazione modulo n............................................................... 181
12.4 Definizione di RSA............................................................................. 182
12.4.1 Firme digitali con RSA........................................................... 182
12.4.2 Gli esponenti pubblici............................................................. 182
12.4.3 La chiave privata..................................................................... 183
12.4.4 La dimensione di n.................................................................. 184
12.4.5 Generazione di chiavi RSA..................................................... 185
12.5 I pericoli dell’utilizzo di RSA.............................................................. 186
12.6 Cifratura.............................................................................................. 187
12.7 Firme.................................................................................................. 190
12.8 Esercizi................................................................................................ 192

x  Il manuale della crittografia

Capitolo 13 Introduzione ai protocolli crittografici........................ 193
13.1 I ruoli.................................................................................................. 193
13.2 La fiducia............................................................................................ 194
13.2.1 Rischio................................................................................... 195
13.3 Gli incentivi........................................................................................ 195
13.4 La fiducia nei protocolli crittografici.................................................... 197
13.5 Messaggi e attività................................................................................ 198
13.5.1 Il livello di trasporto................................................................ 198
13.5.2 Identità del protocollo e del messaggio.................................... 199
13.5.3 Codifica e analisi del messaggio............................................... 200
13.5.4 Stati di esecuzione del protocollo............................................ 200
13.5.5 Errori...................................................................................... 201
13.5.6 Attacchi con replica e nuovi tentativi....................................... 202
13.6 Esercizi................................................................................................ 204

Capitolo 14 Il protocollo di negoziazione della chiave................... 205
14.1
14.2
14.3
14.4
14.5
14.6
14.7
14.8

Lo scenario.......................................................................................... 205
Un primo tentativo.............................................................................. 206
I protocolli sono eterni........................................................................ 207
Una convenzione di autenticazione...................................................... 208
Un secondo tentativo........................................................................... 209
Un terzo tentativo............................................................................... 210
Il protocollo finale............................................................................... 211
Visioni diverse del protocollo............................................................... 213
14.8.1 La visione di Alice................................................................... 213
14.8.2 La visione di Bob.................................................................... 213
14.8.3 La visione dell’attaccante......................................................... 214
14.8.4 Compromissione della chiave.................................................. 215
14.9 Complessità computazionale del protocollo.......................................... 215
14.9.1 Trucchi di ottimizzazione........................................................ 216
14.10 Complessità del protocollo................................................................. 217
14.11 Un avvertimento............................................................................... 218
14.12 Negoziazione della chiave a partire da una password........................... 218
14.13 Esercizi.............................................................................................. 218

Capitolo 15 Aspetti relativi all’implementazione (II)...................... 219
15.1 Calcoli aritmetici su interi grandi......................................................... 219
15.1.1 Wooping................................................................................. 221
15.1.2 Verifica dei calcoli DH............................................................ 223
15.1.3 Verifica della cifratura RSA...................................................... 224
15.1.4 Verifica delle firme RSA.......................................................... 224
15.1.5 Conclusioni............................................................................ 224
15.2 Una moltiplicazione più rapida............................................................ 224
15.3 Attacchi a canale laterale...................................................................... 225
15.3.1 Contromisure.......................................................................... 226
15.4 I protocolli.......................................................................................... 227
15.4.1 Protocolli su un canale sicuro.................................................. 228
15.4.2 Ricezione di un messaggio...................................................... 228
15.4.3 Timeout.................................................................................. 229
15.5 Esercizi................................................................................................ 230

Indice generale   xi

Parte IV

Gestione delle chiavi................................231

Capitolo 16 L’orologio........................................................................ 233
16.1 Impieghi di un orologio....................................................................... 233
16.1.1 Scadenza................................................................................. 233
16.1.2 Valore unico............................................................................ 234
16.1.3 Monotonicità.......................................................................... 234
16.1.4 Transazioni in tempo reale....................................................... 234
16.2 Utilizzo del chip RTC......................................................................... 235
16.3 Pericoli per la sicurezza........................................................................ 235
16.3.1 Portare indietro l’orologio....................................................... 235
16.3.2 Fermare l’orologio.................................................................. 236
16.3.3 Portare avanti l’orologio.......................................................... 237
16.4 Creazione di un orologio affidabile...................................................... 237
16.5 Il problema dello stato che non cambia................................................ 238
16.6 L’orario di riferimento......................................................................... 240
16.7 Conclusioni e consigli.......................................................................... 240
16.8 Esercizi................................................................................................ 241

Capitolo 17 Server di chiavi............................................................... 243
17.1 Concetti di base................................................................................... 244
17.2 Kerberos.............................................................................................. 244
17.3 Soluzioni più semplici.......................................................................... 245
17.3.1 Connessione sicura.................................................................. 245
17.3.2 Impostazione di una chiave..................................................... 246
17.3.3 Rinnovo della chiave............................................................... 246
17.3.4 Altre proprietà......................................................................... 246
17.4 Che cosa scegliere................................................................................ 247
17.5 Esercizi................................................................................................ 247

Capitolo 18 Le PKI dei sogni.............................................................. 249
18.1 Breve panoramica sulle PKI................................................................. 249
18.2 Esempi di PKI..................................................................................... 250
18.2.1 La PKI universale.................................................................... 250
18.2.2 Accesso con VPN.................................................................... 250
18.2.3 Banca elettronica..................................................................... 250
18.2.4 Sensori di una raffineria.......................................................... 251
18.2.5 Circuiti di carte di credito....................................................... 251
18.3 Altri elementi...................................................................................... 251
18.3.1 Certificati multilivello............................................................. 251
18.3.2 Scadenza................................................................................. 252
18.3.3 Autorità di registrazione separata............................................. 253
18.4 Riepilogo............................................................................................ 254
18.5 Esercizi................................................................................................ 254

Capitolo 19 Le PKI nella realtà.......................................................... 255
19.1
19.2
19.3
19.4
19.5
19.6

Nomi.................................................................................................. 255
Autorità............................................................................................... 257
Fiducia................................................................................................ 257
Autorizzazione indiretta....................................................................... 258
Autorizzazione diretta.......................................................................... 259
Sistemi di credenziali........................................................................... 260

xii  Il manuale della crittografia
19.7 Il sogno rivisitato................................................................................. 261
19.8 La revoca............................................................................................. 262
19.8.1 Elenco delle revoche............................................................... 262
19.8.2 Scadenza rapida....................................................................... 263
19.8.3 Verifica di certificati online...................................................... 263
19.8.4 Revoca obbligatoria................................................................ 264
19.9 In conclusione, a che cosa serve una PKI?............................................ 264
19.10 Che cosa scegliere.............................................................................. 266
19.11 Esercizi.............................................................................................. 266

Capitolo 20 Considerazioni pratiche sulle PKI................................. 267
20.1 Il formato del certificato...................................................................... 267
20.1.1 Il linguaggio per i permessi..................................................... 267
20.1.2 La chiave di root..................................................................... 268
20.2 La vita di una chiave............................................................................ 269
20.3 Perché le chiavi si usurano................................................................... 270
20.4 Il cammino prosegue........................................................................... 271
20.5 Esercizi................................................................................................ 271

Capitolo 21 Come conservare i segreti............................................. 273
21.1 Unità a disco....................................................................................... 273
21.2 Memoria umana.................................................................................. 274
21.2.1 Sale e allungamento................................................................ 275
21.3 Supporti di archiviazione portatili........................................................ 277
21.4 Dispositivi sicuri.................................................................................. 278
21.5 Interfaccia utente sicura....................................................................... 279
21.6 Biometria............................................................................................ 279
21.7 Sistemi SSO (Single Sign-On)............................................................. 280
21.8 Rischio di perdita dei dati.................................................................... 281
21.9 Secret sharing...................................................................................... 281
21.10 Cancellazione dei segreti.................................................................... 282
21.10.1 Carta..................................................................................... 282
21.10.2 Supporti magnetici................................................................ 283
21.10.3 Supporti a stato solido........................................................... 284
21.11 Esercizi.............................................................................................. 284

Parte V

Miscellanea...............................................285

Capitolo 22 Standard e brevetti........................................................ 287
22.1 Standard.............................................................................................. 287
22.1.1 I l processo di creazione degli standard...................................... 287
22.1.2 SSL......................................................................................... 290
22.1.3 AES: standardizzazione attraverso la concorrenza...................... 291
22.2 Brevetti................................................................................................ 292

Capitolo 23 Rivolgersi agli esperti.................................................... 293
Bibliografia ......................................................................................... 297
Indice analitico...................................................................................... 305

Nota dell’editore

La prima edizione del libro che avete tra le mani è stata pubblicata dall’editore John Wiley
& Sons nel 2003 con il titolo Practical Cryptography e tradotta in italiano da Apogeo nel
2005 con il titolo Crittografia pratica.
Da allora le trasformazioni avvenute nel mondo della crittografia sono state tante e tali
da indurre gli autori e l’editore originale a pubblicare una nuova edizione del testo che
si discosta sensibilmente dalla prima al punto da avere un titolo completamente nuovo,
Cryptography Engineering, e un nuovo autore,Tadayoshi Kohno, che si è aggiunto a Niels
Ferguson e Bruce Schneier.
Quanto segue è la traduzioe di Cryptography Engineering. Per meglio comprendere l’evoluzione e la genesi di questo testo troverete anche la prefazione di Practical Cryptography.
Buona lettura.

A Denise, che mi ha reso davvero felice.
– Niels Ferguson
A Karen; ancora, dopo tutti questi anni.
– Bruce Schneier
A Taryn, per rendere tutto possibile.
– Tadayoshi Kohno

Prefazione alla prima edizione

Nell’ultimo decennio la crittografia ha portato più danni che benefici alla sicurezza dei
sistemi digitali. La crittografia è balzata sulla scena mondiale nei primi anni ’90, per il
suo ruolo a protezione di Internet. Alcuni la vedevano come una sorta di equalizzatore
tecnologico, uno strumento matematico che avrebbe portato i più umili individui alla
ricerca di privacy sullo stesso piano delle più grandi agenzie di intelligence nazionali.
Alcuni la vedevano come l’arma che avrebbe causato la caduta degli stati ove i governi
avessero perso la capacità di controllare le persone nel ciberspazio.Altri la vedevano come
lo strumento perfetto e terribile nelle mani di trafficanti di droga, terroristi e pedofili,
che sarebbero stati in grado di comunicare in perfetta segretezza. Anche i più realisti
immaginavano la crittografia come una tecnologia che avrebbe consentito la nascita del
commercio globale nel nuovo mondo online.
Dieci anni dopo, nessuna di queste previsioni si è avverata. Nonostante l’avvento della crittografia, i confini nazionali di Internet sono più visibili che mai. La capacità di
individuare e intercettare comunicazioni criminali riguarda più la politica e le risorse
umane che la matematica. Gli individui sono ancora inermi contro le potenti e ricche
agenzie governative. E la diffusione del commercio globale non ha nulla a che fare con
l’avvento della crittografia.
Per la maggior parte, la crittografia non ha fatto che dare agli utenti di Internet un falso
senso di sicurezza, promettendo protezione senza fornirla davvero. E non è una buona
notizia, salvo per i malintenzionati.
I motivi di tutto ciò non riguardano tanto la crittografia come scienza matematica, ma
più la crittografia come disciplina tecnica. In tutto l’ultimo decennio abbiamo sviluppato, implementato e messo in opera sistemi crittografici, ma non siamo stati capaci di
trasformare in realtà la promessa matematica della sicurezza crittografica. Evidentemente,
questa è la parte difficile.
Troppi tecnici considerano la crittografia come una sorta di polvere magica che possono
spargere su hardware o software per inoculare in quei prodotti la mitica proprietà della
“sicurezza”. Troppi consumatori leggono slogan pubblicitari come “cifrato” e credono
nella stessa polvere magica. Chi scrive recensioni non fa di meglio, mettendo a confronto
aspetti come la lunghezza delle chiavi e, su quelle basi, affermando che un prodotto è
più sicuro di un altro.

xvi  Il manuale della crittografia

Il livello di sicurezza è determinato dall’elemento più debole e la matematica della
crittografia non è quasi mai l’elemento più debole. I fondamenti della crittografia sono
importanti, ma molto più importante è il modo in cui sono implementati e usati. Discutere se una chiave debba essere lunga 112 o 128 bit è come infilare un altissimo palo
nel terreno e sperare che l’attaccante ci sbatta contro. Potete ragionare sul fatto che il
palo debba esse alto un chilometro o uno e mezzo, ma intanto l’attaccante non farà altro
che aggirarlo. La sicurezza è una larga palizzata: sono gli elementi che stanno attorno alla
crittografia che la rendono efficace.
I libri di crittografia dell’ultimo decennio hanno contribuito all’aura di magia che
abbiamo descritto. Un libro dopo l’altro esaltava le virtù del triple-DES a 112 bit, per
esempio, senza dire molto sul modo in cui le sue chiavi andassero generate o utilizzate.
Un libro dopo l’altro presentava complicati protocolli per questo o quell’altro, senza citare
i vincoli economici e sociali in cui questi protocolli dovevano operare. Un libro dopo
l’altro spiegava la crittografia come concetto di matematica pura, non contaminato dai
vincoli e le condizioni del mondo reale. Eppure sono proprio i vincoli e le condizioni
del mondo reale che fanno la differenza tra la promessa di una magia crittografica e la
realtà della sicurezza digitale.
Practical Cryptography è anche un libro sulla crittografia, ma prima di tutto è un libro
sulla crittografia contaminata. Il nostro scopo è proprio quello di descrivere i vincoli e
le condizioni che la crittografia deve affrontare nel mondo reale e di parlare di come
realizzare sistemi crittografici sicuri. Per certi versi è il seguito del primo libro di Bruce Schneier, Applied Cryptography, pubblicato per la prima volta dieci anni fa. Tuttavia,
mentre Applied Cryptography forniva un’ampia panoramica sulla crittografia e le miriadi
di possibilità che può offrire, questo libro è più circoscritto. Non forniamo decine di
scelte, ma una sola, indicandovi come implementarla correttamente. Applied Cryptography
presenta le meravigliose possibilità della crittografia come scienza matematica, ciò che
è possibile e ciò che è realizzabile. Questo libro fornisce consigli pratici a chi progetta
e implementa sistemi crittografici.
Practical Cryptography è il nostro tentativo di colmare il gap tra la promessa e la realtà
della crittografia; di insegnare ai tecnici come utilizzare la crittografia per aumentare la
sicurezza.
Siamo ben qualificati a scrivere questo libro, perché entrambi lavoriamo da tempo nel
campo della crittografia. Bruce è noto per i suoi libri Applied Cryptography e Secrets and
Lies e per la sua newsletter “Crypto-Gram”. Niels Ferguson ha fatto esperienza nel
campo realizzando sistemi di pagamento crittografici presso il CWI (Dutch National
Research Institute for Mathematics and Computer Science) ad Amsterdam e poi nella
società olandese DigiCash. Bruce ha progettato l’algoritmo di cifratura Blowfish ed
entrambi abbiamo fatto parte del team che ha progettato Twofish. Le ricerche di Niels
hanno portato al primo esempio dell’attuale generazione di protocolli di pagamento
anonimi ed efficienti. Mettendo insieme tutti gli articoli che abbiamo scritto arriviamo
a numeri di tre cifre.
Inoltre, entrambi abbiamo una notevole esperienza nella progettazione e nella realizzazione di sistemi crittografici. Dal 1991 a 1999 la società di consulenza di Bruce, Counterpane Systems, ha fornito servizi di progettazione e analisi ad alcune tra le più importanti
società di informatica e finanziarie del mondo. In anni più recenti, Counterpane Internet
Security, Inc., ha fornito servizi di monitoraggio gestito della sicurezza a grandi imprese

Prefazione alla prima edizione   xvii

ed enti pubblici in tutto il mondo. Anche Niels ha lavorato in Counterpane prima di
fondare la propria società di consulenza, MacFergus. Abbiamo visto la crittografia vivere
e respirare nel mondo reale, affrontare le realtà della tecnica o, ancora peggio, del business.
Siamo qualificati a scrivere questo libro perché abbiamo già dovuto scriverlo tante volte
per i nostri clienti che ci chiedevano consulenza.

Come leggere questo libro
Practical Cryptography è più simile a una narrazione che a un manuale di riferimento. Segue
la progettazione di un sistema crittografico a partire dalle scelte specifiche dell’algoritmo,
procedendo per anelli concentrici sempre più estesi fino a raggiungere l’infrastruttura
richiesta per farlo funzionare. Discutiamo un unico problema crittografico, quello di
stabilire una modalità di comunicazione sicura tra due persone, che rappresenta il cuore
di quasi tutte le applicazioni di crittografia. Riteniamo che, concentrandoci su un unico
problema e un’unica filosofia di progettazione per risolverlo, possiamo spiegare meglio
le condizioni reali in cui si lavora nel campo della crittografia.
Pensiamo che la crittografia sia forse l’applicazione più divertente della matematica. Abbiamo cercato di far vivere in questo libro questa sensazione di divertimento, e speriamo
che gradirete il risultato. Grazie per aver deciso di accompagnarci nel nostro viaggio.
Niels Ferguson
Bruce Schneier
Gennaio 2003

Ringraziamenti per la prima edizione

Questo libro si basa sulla nostra esperienza collettiva maturata nei molti anni in cui ci
siamo occupati di crittografia. Siamo in debito con tutte le persone con le quali abbiamo
lavorato: ci hanno fatto amare il nostro lavoro e ci hanno aiutato a formare le nostre
conoscenze che abbiamo riportato in questo libro. Desideriamo ringraziare anche tutti
i nostri clienti per averci fornito sia i fondi che ci hanno consentito di continuare le ricerche nella crittografia, sia le esperienze concrete necessarie per scrivere questo libro.
Alcune persone meritano una menzione speciale. Beth Friedman ha svolto un lavoro
di copyediting di inestimabile valore e Denise Dick ha notevolmente migliorato il manoscritto con un’accurata correzione di bozze. John Kelsey ha fornito utili feedback sui
contenuti. E Internet ha reso possibile la nostra collaborazione. Desideriamo ringraziare
anche Carol Long e gli altri membri del team di Wiley per aver trasformato le nostre
idee in realtà.
Infine, desideriamo ringraziare tutti i programmatori di tutto il mondo che continuano
a scrivere codice crittografico e a renderlo disponibile a tutti, gratuitamente.

Prefazione alla nuova edizione

La maggior parte dei libri spiega che cos’è la crittografia, quali sono gli schemi attuali e
come funzionano i protocolli crittografici esistenti, come SSL/TLS. Il precedente libro
di Bruce Schneier, Applied Cryptography, è uno di questi. Questi libri forniscono utilissimi
riferimenti per chiunque lavori con la crittografia, ma si mantengono a un passo di distanza dalle esigenze concrete dei crittografi e dei tecnici della sicurezza. A chi si occupa
di crittografia e di sicurezza non basta sapere come funzionano i protocolli crittografici
attuali, devono sapere come utilizzare la crittografia, e per raggiungere questo scopo è
necessario imparare a pensare come un professionista del campo.
Questo libro è stato pensato per aiutarvi a raggiungere tale scopo, utilizzando la strategia
dell’immersione: anziché discutere ampiamente i protocollo che potreste incontrare
nella crittografia, ci addentreremo profondamente nella progettazione e nell’analisi di
protocolli specifici, utilizzati nella pratica.Vi condurremo per mano nella progettazione
di protocolli crittografici, condividendo con voi i motivi per cui preferiamo determinate
scelte rispetto ad altre ed evidenziando le insidie presenti lungo la strada.
Imparando a pensare come un professionista della crittografia, imparerete anche a diventare
un utente più perspicace. Sarete in grado di esaminare i toolkit crittografici disponibili,
di capirne le funzionalità centrali e di utilizzarli al meglio. Capirete meglio anche le sfide
della crittografia e come tentare di superarle.
Questo libro serve anche per introdurvi al campo della sicurezza informatica.
La sicurezza informatica è per molti aspetti una sovrainsieme della crittografia. Entrambe
le discipline trattano la progettazione e la valutazione di oggetti (sistemi o algoritmi)
pensati per comportarsi in determinati modi anche in presenza di un avversario. In
questo libro imparerete a pensare all’avversario nel contesto della crittografia. Quando
avrete imparato a pensare come il nemico, potrete applicare tale mentalità al campo della
sicurezza informatica in generale.

Evoluzione del libro
La prima edizione di questo libro, con il titolo Practical Cryptography, è stata scritta da
Niels Ferguson e Bruce Schneier. In seguito agli autori si è aggiunto Tadayoshi Kohno – Yoshi per gli amici. Yoshi è professore di Computer Science and Engineering
all’Università di Washington e in passato è stato collega di Niels e Bruce; ha lavorato su

xx  Il manuale della crittografia

Practical Cryptography per adattarlo all’utilizzo in corsi collettivi e per lo studio personale,
mantenendo sempre validi gli obiettivi e i temi fondamentali del libro originale scritto
da Niels e Bruce.

Percorsi didattici di esempio
Questo libro può essere letto in vari modi. Potete utilizzarlo come guida di autoapprendimento per la crittografia applicata, oppure in un normale corso di studio. All’interno
di un corso semestrale di sicurezza informatica, il libro potrebbe servire da base per un
percorso intensivo di sei settimane dedicato alla crittografia. Oppure potrebbe costituire
il riferimento di un corso semestrale sulla crittografia, eventualmente con l’aggiunta di
materiali più avanzati qualora il tempo lo consenta. Per facilitarne l’utilizzo nei corsi di
studio, presentiamo di seguito alcuni possibili percorsi didattici.
Il syllabus che segue è adatto per un percorso intensivo di 6 settimane sulla crittografia.
Presupponiamo in questo caso che il contenuto del Capitolo 1 sia trattato separatamente,
nel contesto più ampio della sicurezza informatica.
Settimana 1: Capitoli 2, 3 e 4
Settimana 2: Capitoli 5, 6 e 7
Settimana 3: Capitoli 8, 9 e 10
Settimana 4: Capitoli 11, 12 e 13
Settimana 5: Capitoli 14, 15, 16 e 17
Settimana 6: Capitoli 18, 19, 20 e 21
Il syllabus che segue è adatto per un percorso trimestrale di 10 settimane sulle tecniche
crittografiche.
Settimana 1: Capitoli 1 e 2
Settimana 2: Capitoli 3 e 4
Settimana 3: Capitoli 5 e 6
Settimana 4: Capitoli 7 e 8
Settimana 5: Capitoli 9 e 10
Settimana 6: Capitoli 11 e 12
Settimana 7: Capitoli 13 e 14
Settimana 8: Capitoli 15, 16 e 17
Settimana 9: Capitoli 18, 19, 20
Settimana 10: Capitolo 21
Il syllabus che segue è adatto per corsi semestrali di 12 settimane. Può anche essere
esteso con materiali avanzati sulla crittografia o la sicurezza informatica, per periodi più
lunghi.
Settimana 1: Capitoli 1 e 2
Settimana 2: Capitoli 3 e 4
Settimana 3: Capitoli 5 e 6
Settimana 4: Capitolo 7

Prefazione alla nuova edizione   xxi

Settimana 5: Capitoli 8 e 9
Settimana 6: Capitoli 9 (continua) e 10
Settimana 7: Capitoli 11 e 12
Settimana 8: Capitoli 13 e 14
Settimana 9: Capitoli 15 e 16
Settimana 10: Capitoli 17 e 18
Settimana 11: Capitoli 19 e 20
Settimana 12: Capitolo 21
Il libro contiene diversi tipi di esercizi e incoraggiamo i lettori a svolgerne il più possibile. Ci sono esercizi tradizionali progettati per verificare la comprensione degli aspetti
tecnici della crittografia. Tuttavia, poiché il nostro scopo è quello di aiutarvi a imparare
come pensare alla crittografia nel contesto di sistemi reali, abbiamo introdotto anche
alcuni esercizi non tradizionali (cfr. Paragrafo 1.12). La crittografia non è un mondo
isolato, ma fa parte di un ecosistema più ampio, costituito da altri sistemi hardware e
software, persone, economia, etica, differenze culturali, politica, legislazione e così via.
I nostri esercizi non tradizionali sono progettati proprio per costringervi a pensare alla
crittografia nel contesto di sistemi reali e dell’ecosistema che li circonda. Questi esercizi
vi forniranno l’opportunità di applicare direttamente i contenuti del libro come esercizi
di riflessione sui sistemi reali. Inoltre, intrecciando insieme gli esercizi in tutto il libro,
sarete in grado di vedere come crescono le vostre conoscenze mentre progredite nella
lettura capitolo dopo capitolo.

Informazioni aggiuntive
Nonostante tutti gli sforzi profusi nella revisione del libro, qualche errore sarà certamente presente. È disponibile online un elenco di errata presso il sito di Bruce Schneier,
utilizzabile nel modo seguente.
Prima di leggere questo libro, collegatevi a http://www.schneier.com/ce.html e prelevate
l’elenco di correzioni aggiornato.
Se trovate un errore nel libro, verificate se è già citato nell’elenco. Se non lo è, segnalatelo
via email a [email protected]. Lo aggiungeremo all’elenco.
Vi auguriamo uno splendido viaggio nel mondo della crittografia. Il tema è bellissimo e
affascinante. Speriamo che possiate imparare molto da questo libro e arrivare ad amare
la crittografia come l’amiamo noi.
Ottobre 2009

Niels Ferguson
Redmond, Washington
USA

Bruce Schneier
Minneapolis, Minnesota
USA

[email protected]

[email protected]

Tadayoshi Kohno
Seattle, Washington
USA
[email protected]

Ringraziamenti per la nuova edizione

Siamo in debito con la comunità della crittografia e della sicurezza. Questo libro non
sarebbe stato possibile senza tutto il loro impegno per far progredire il campo. Il libro
inoltre riflette la nostra conoscenza ed esperienza nel campo e siamo profondamente
grati a compagni e mentori per averci aiutato a forgiare la nostra comprensione della
crittografia.
Ringraziamo Jon Callas, Ben Greenstein, Gordon Goetz, Alex Halderman, John Kelsey,
Karl Koscher, Jack Lloyd, Gabriel Maganis,Theresa Portzer, Jesse Walker, Doug Whiting,
Zooko Wilcox-O’Hearn e Hussein Yapit per averci fornito utilissimi commenti sulle
precedenti versioni di questo libro.
Parte di questo libro è stata sviluppata e affinata in un corso di sicurezza informatica
tenuto presso l’Università di Washington. Ringraziamo tutti gli studenti, gli assistenti
e i mentori per il corso. Un ringraziamento speciale a Joshua Barr, Jonathan Beall, Iva
Dermendjieva, Lisa Glendenning, Steven Myhre, Erik Turnquist e Heather Underwood
per aver fornito specifici commenti e suggerimenti relativi al testo.
Ringraziamo Melody Kadenko e Julie Svendsen per tutto il supporto amministrativo
fornito nell’intero processo di lavoro. Siamo in debito con Beth Friedman per il lavoro
di copyediting. Infine, ringraziamo Carol Long, Tom Dinse e tutto il team di Wiley per
averci incoraggiato a scrivere questo libro e per averci aiutato lungo tutto il percorso.
Siamo in debito anche con tutte le altre persone meravigliose che ci circondano e che
hanno lavorato in silenzio dietro le quinte per rendere possibile questo libro.

Gli autori

Niels Ferguson ha trascorso l’intera carriera nel campo della crittografia. Dopo gli
studi in matematica a Eindhoven, ha lavorato per DigiCash nell’analisi, progettazione
e implementazione di sistemi avanzati di pagamento elettronico che proteggessero la
privacy dell’utente. In seguito è stato consulente per Counterpane e MacFergus, analizzando centinaia di sistemi crittografici e progettandone decine. Ha fatto parte del team
che ha progettato il cifrario a blocchi Twofish, ha svolto alcune tra le migliori analisi
iniziali di AES e ha partecipato alla progettazione del sistema di cifratura attualmente
utilizzato da WiFi. Dal 2004 lavora in Microsoft, dove ha contribuito alla progettazione
e all’implementazione del sistema di cifratura del disco BitLocker.Attualmente opera nel
team per la crittografia di Windows, responsabile delle implementazioni crittografiche
in Windows e altri prodotti di Microsoft.
Bruce Schneier è un tecnico della sicurezza rinomato a livello internazionale, citato
da The Economist come “guru della sicurezza”. È autore di otto libri, tra cui i best seller
Beyond Fear:Thinking Sensibly about Security in an Uncertain World, Secrets and Lies e Applied
Cryptography, oltre che di centinaia di articoli e saggi pubblicati in tutto il mondo, e di
numerosi articoli accademici. La sua newsletter Crypto-Gram e il suo blog Schneier on
Security (http://www.schneier.com/) sono letti da oltre 250.000 persone. È spesso ospite
alla televisione e alla radio e viene regolarmente citato sulla stampa in relazione a temi
di sicurezza e privacy. In più occasioni ha reso testimonianza al Congresso degli Stati
Uniti e ha fornito i sui servigi a varie commissioni tecniche del governo. Schneier è
Chief Security Technology Officer di BT.
Tadayoshi Kohno (Yoshi) è assistente di Computer Science and Engineering presso
l’Università di Washington. Le sue ricerche si concentrano sul miglioramento delle
caratteristiche di sicurezza e privacy delle tecnologie attuali e future. Ha condotto la
prima analisi della sicurezza del codice sorgente per la macchina di votazione elettronica
Diebold AccuVote-TS nel 2003 e da allora ha puntato la propria attenzione alla messa in
sicurezza delle tecnologie emergenti, dai pacemaker wireless ai defibrillatori fino al cloud
computing. Ha ricevuto un National Science Foundation CAREER Award e un Alfred
P. Sloan Research Fellowship. Nel 2007 ha ricevuto il MIT Technology Review TR-35
Award per il suo lavoro sulla crittografia applicata, venendo riconosciuto come uno tra
i primi innovatori al mondo tra i minori di 35 anni. Ha ricevuto il PhD in Computer
Science dall’Università della California a San Diego.
Niels, Bruce e Yoshi fanno parte del team che ha progettato la funzione di hash Skein,
tra i concorrenti al concorso SHA-3 del NIST.

Parte I

Introduzione

In questa parte
• Capitolo 1 Il contesto della crittografia
• Capitolo 2 Introduzione alla crittografia

Capitolo 1

Il contesto
della crittografia
La crittografia è l’arte e la scienza della cifratura. O
almeno così era alle origini, mentre oggi si è ampliata
enormemente, arrivando a comprendere autenticazione, firme digitali e molte funzioni di sicurezza
più elementari. Rimane però un’arte e una scienza:
per costruire sistemi crittografici ben fatti servono
cultura scientifica e una buona dose di una pozione
magica costituita da una miscela di esperienza e della
mentalità adatta a riflettere su problemi di sicurezza.
Questo libro è stato scritto per aiutarvi a coltivare
questi ingredienti fondamentali.
Il campo della crittografia è estremamente vario. Nelle conferenze si affrontano gli argomenti più vari, tra
cui sicurezza dei computer, algebra superiore, economia, fisica quantistica, giurisprudenza civile e penale,
statistica, progettazione di chip, ottimizzazione di
software avanzata, politica, progettazione di interfacce
utente e tanto altro. Questo libro però si concentra
su una parte molto limitata, in un certo senso: il lato
pratico. Cerchiamo di insegnare come implementare
la crittografia in sistemi del mondo reale. Per altri
aspetti, invece, il libro offre una visione molto più
ampia, poiché consente di acquisire esperienza nelle
tecniche della sicurezza e di coltivare la propria capacità di riflettere sulla crittografia e sui temi della
sicurezza dalla prospettiva di un professionista del settore. In questo modo vi risulterà più facile affrontare
con successo le sfide della sicurezza, anche ove non
siano direttamente legate alla crittografia.
È proprio la varietà del campo a rendere tanto affascinante la crittografia, che è davvero una miscela di
campi molto diversi. C’è sempre qualcosa di nuovo
da imparare e nuovi spunti che provengono da tutte

Sommario
In questa parte
1.1 Il ruolo della crittografia
1.2 La proprietà
dell’elemento più debole
1.3 Un ambiente ostile
1.4 Paranoia professionale
1.5 Il modello delle minacce
1.6 La crittografia non è
la soluzione
1.7 La crittografia è molto
difficile
1.8 La crittografia è la parte
facile
1.9 Attacchi generici
1.10 La sicurezza e altri
criteri di progettazione
1.11 Letture consigliate
1.12 Esercizi per la paranoia
professionale
1.13 Esercizi generali

4   Capitolo 1

le direzioni. Questo è anche uno dei motivi per cui la crittografia è un campo difficile: è
impossibile dire di conoscerlo tutto, o anche solo la maggior parte. Noi autori certamente
non conosciamo tutto ciò che c’è da conoscere sull’argomento di questo libro. Ecco quindi
la prima lezione di crittografia: mantenere una mentalità critica. Non fidarsi ciecamente
di nulla, nemmeno se è stampato in un libro. Presto vedrete che questa mentalità è un
ingrediente essenziale di ciò che chiamiamo “paranoia professionale”.

1.1 Il ruolo della crittografia
La crittografia di per sé è praticamente inutile; deve essere utilizzata all’interno di un
sistema molto più ampio. Ci piace il paragone con le serrature: una serratura di per sé
è un oggetto inutile; deve far parte di un sistema molto più grande, che può essere una
porta, una catena o qualcosa del genere. Questo sistema più grande si estende anche alle
persone che dovranno usare la serratura, e che dovranno ricordare di chiuderla e di non
lasciare la chiave a disposizione di tutti. Lo stesso vale per la crittografia: è soltanto una
piccola parte di un sistema di sicurezza molto più ampio.
Sì, la crittografia è una piccola parte del sistema di sicurezza, ma è una parte fondamentale,
quella responsabile di concedere l’accesso ad alcune persone ma non ad altre. Questo è
un aspetto molto delicato. La maggior parte degli elementi del sistema di sicurezza sono
simili a muri e recinti, nel senso che sono progettati per tenere fuori tutti. La crittografia
assume il ruolo della serratura: deve distinguere tra accesso “buono” e accesso “cattivo”,
cosa molto più difficile. Per questo motivo la crittografia e i suoi elementi di contorno
costituiscono un punto di attacco naturale per qualsiasi sistema di sicurezza.
Ciò non significa che la crittografia sia sempre il punto debole. In alcuni casi, anche una
cattiva crittografia può essere molto migliore di tutto il resto del sistema. Probabilmente
avrete visto le porte dei caveau delle banche, almeno al cinema: pesante acciaio temperato
da 25 cm di spessore, con bulloni e cardini enormi. Davvero impressionanti da vedere.
Nel mondo digitale spesso troviamo l’equivalente di queste porte installate su sistemi
simili a tende da campeggio. E le persone che se ne occupano pensano allo spessore che
dovrebbe avere la porta, anziché occuparsi della tenda. È facile perdere ore e ore a ragionare
sulla lunghezza della chiave di un sistema crittografico, senza accorgersi della presenza
di un problema di buffer overflow in un’applicazione web. Il risultato è prevedibile: gli
attaccanti trovano il buffer overflow e non si preoccupano del resto. La crittografia è utile
davvero soltanto se anche il resto del sistema è sufficientemente sicuro.
Esistono comunque dei motivi per cui è importante utilizzare una crittografia corretta
anche in sistemi che presentano altri punti deboli. Infatti, punti deboli diversi possono
risultare utili ad attaccanti diversi in modi diversi. Per esempio, ci sono scarse probabilità di individuare un attaccante che violi la crittografia, dato che non ci saranno tracce
dell’attacco, poiché l’accesso utilizzato dal malintenzionato apparirà del tipo “buono”.
Succede anche nel mondo reale: se un ladro usa una mazza per rompere una porta, si
vedranno i segni dell’effrazione, ma se è in grado di aprire la serratura, potrebbe risultare
impossibile accorgersi del furto. Molte modalità di attacco lasciano delle tracce o causano
vari tipi di disturbi al sistema. Un attacco alla crittografia, invece, può essere invisibile, e
per questo potrebbe essere sfruttato da un attaccante più e più volte.

Il contesto della crittografia    5

1.2 La proprietà dell’elemento più debole
Stampate la frase seguente a caratteri cubitali e incollatela sopra il monitor:
Un sistema di sicurezza è forte quanto lo è il suo elemento più debole.
Osservate queste parole ogni giorno e cercate di capirne le implicazioni. La proprietà
dell’elemento più debole è uno dei motivi principali per cui è così terribilmente difficile
realizzare buoni sistemi di sicurezza.
Ogni sistema di sicurezza è costituito da un gran numero di elementi. Dobbiamo ipotizzare che il nostro avversario sia intelligente e che tenterà di attaccare il sistema nel
suo punto più debole. Non importa quanto forti siano gli altri elementi: come in una
catena, l’anello più debole sarà quello che si romperà per primo, a prescindere dalla
robustezza degli altri.
Niels Ferguson lavorava in una palazzina di uffici in cui tutte le porte dei locali venivano
chiuse ogni notte. Sembra molto sicuro, vero? Il problema era che l’edificio aveva un
controsoffitto. Bastava sollevare i pannelli del controsoffitto per poter “scavalcare” agevolmente qualsiasi porta o parete. Eliminando i pannelli, tutto il piano poteva assomigliare a
una serie di cubi aperti e dotati di porte. E le porte avevano serrature. Chiudere a chiave
le porte rendeva un po’ più difficile l’ingresso a un ladro, ma appesantiva il lavoro delle
guardie che controllavano gli uffici nelle ore notturne. Non è affatto chiaro se la sicurezza
complessiva migliorasse o peggiorasse, chiudendo le porte a chiave. In questo esempio,
la proprietà dell’elemento più debole riduce notevolmente l’efficacia dell’operazione di
chiusura a chiave delle porte. Tale operazione, infatti, aumenta la forza di un particolare
elemento (la porta), ma ne esiste un altro (il soffitto) che rimane debole, perciò l’effetto
complessivo è limitatissimo, e potrebbe anche essere superato dagli effetti collaterali
negativi che comporta.
Per migliorare la sicurezza di un sistema occorre migliorare l’elemento più debole, ma
per fare ciò è necessario conoscere quali sono gli elementi e quali sono i più deboli.
A questo scopo si utilizza una struttura ad albero gerarchica. Ogni parte del sistema ha
più elementi, ognuno dei quali a sua volta ha dei sottoelementi. Possiamo disporre gli
elementi in un attack tree o albero di attacco [113], come nell’esempio della Figura 1.1.
Supponiamo di voler entrare nel caveau di una banca. Gli elementi di primo livello sono
i muri, il pavimento, la porta e il soffitto: violandone uno possiamo entrare nel caveau.
Esaminiamo meglio la porta; è un sistema che contiene vari elementi: il collegamento
tra il suo telaio e i muri, la serratura, la struttura stessa della porta, i tasselli che la fissano
al telaio, i cardini. Potremmo continuare esaminando singole linee di attacco alla serratura, una delle quali consiste nel procurarsi una chiave, che a sua volta condurrebbe a
un albero per il furto di una chiave.
Possiamo analizzare ciascun elemento e suddividerlo in altri elementi fino ad arrivare a
componenti singoli. Con un sistema reale, il lavoro richiesto potrebbe essere enorme. Se
ci occupassimo di un ladro che potrebbe rubare dei diamanti da un caveau, l’albero della
Figura 1.1 sarebbe soltanto una parte di un attack tree più grande; un ladro potrebbe
convincere un impiegato a spostare i diamanti dal caveau e rubarli mentre si trovano
all’esterno.

6   Capitolo 1

entrare
nel caveau

attraverso
i muri

attraverso
la connessione
tra porta e muro

attraverso
il pavimento

scassinare
la serratura

attraverso
la porta

rompere
la porta

tagliare
i bulloni

attraverso
il soffitto

spezzare
i cardini

Figura 1.1 Esempio di attack tree per una porta.

Gli attack tree forniscono utili spunti per possibili linee di attacco e tentare di proteggere
un sistema senza prima procedere a un’analisi simile a quella appena descritta porta molto
spesso a inutili perdite di tempo. In questo libro lavoriamo soltanto su componenti limitati,
quelli che si possono risolvere con la crittografia, e non parleremo esplicitamente dei loro
attack tree, tuttavia è necessario che sappiate come utilizzare un attack tree per studiare
un sistema più grande e per valutare il ruolo della crittografia in tale sistema.
La proprietà dell’elemento più debole influisce sul nostro lavoro per molti aspetti. Per
esempio, si è tentati dall’assumere che gli utenti abbiano password adeguate, ma in realtà
non è così. Spesso gli utenti scelgono password semplici e corte, o fanno di tutto, come
scriverle su foglietti che appiccicano al monitor. Non si possono mai ignorare aspetti
come questi, perché influiscono sempre sul risultato finale. Se progettate un sistema che
fornisce agli utenti una nuova password di 12 cifre, generata in modo casuale ogni settimana, potete star certi che alcuni le scriveranno su foglietti volanti. Così si indebolisce
un elemento già debole, a discapito della sicurezza complessiva del sistema.
In teoria è inutile rafforzare altri elementi che non siano quello più debole, ma nella pratica
le cose non sono sempre così nette. L’attaccante potrebbe non sapere quale sia l’elemento
più debole e attaccarne uno leggermente più forte. L’elemento più debole potrebbe
essere diverso per differenti tipi di attaccanti. La forza di ciascun elemento dipende dalle
capacità dell’attaccante, dagli strumenti che egli ha a disposizione e dall’accesso di cui
dispone, oltre che dai suoi obiettivi. In sostanza non è sempre chiaro quale sia l’elemento
più debole, dipende dalla situazione. Per questo motivo vale la pena di rafforzare qualsiasi
elemento che in una situazione particolare potrebbe essere il più debole. Inoltre, è utile
rafforzare più elementi affinché, se uno fallisse, i rimanenti possano ancora fornire una
certa sicurezza. A questo proposito si parla di difesa in profondità.

1.3 Un ambiente ostile
Una delle principali differenze tra i sistemi di sicurezza e quasi tutti gli altri campi
della tecnica è data dall’ambiente di lavoro che prevede il massimo antagonismo. La

Il contesto della crittografia    7

maggior parte dei tecnici e degli ingegneri deve occuparsi di problemi quali temporali,
calore, umidità e usura.Tutti questi fattori influiscono sui progetti, ma i loro effetti sono
abbastanza prevedibili per un ingegnere esperto. Non è così nel campo dei sistemi di
sicurezza, in cui i nostri avversari sono intelligenti, furbi, maliziosi e ingannevoli; non
giocano secondo le regole e sono del tutto imprevedibili. Si tratta di un ambiente molto
più difficile in cui lavorare.
Molti ricorderanno il filmato in cui il ponte sospeso Tacoma Narrows ondeggia e si
piega sotto le raffiche di vento fino a spezzarsi e a cadere nell’acqua. Quel crollo ha
insegnato molto agli ingegneri che si occupano di ponti: le strutture sospese più leggere
possono entrare in risonanza se sollecitate da un forte vento, con conseguenti oscillazioni fino alla rottura. In che modo si può evitare questo problema per i nuovi ponti?
Farli molto più robusti in modo che resistano alle oscillazioni sarebbe troppo costoso; la
tecnica più comune è quella di cambiare l’aerodinamica della struttura. Il piano viabile
è costruito con uno spessore maggiore e diviene così molto più resistente al vento. A
volte si utilizzano le ringhiere come spoiler per fare in modo che il piano viabile non
si comporti come una vela che si solleva al vento. La tecnica funziona perché il vento è
abbastanza prevedibile e non cambia il proprio comportamento tentando attivamente
di distruggere il ponte.
Un tecnico della sicurezza deve tenere in considerazione l’esistenza di “venti maligni”.
Che cosa succederebbe se il vento soffiasse dall’alto e dal basso anziché dai lati, e se cambiasse direzione proprio in corrispondenza della frequenza che fa entrare in risonanza
il ponte? Gli ingegneri che si occupano di ponti chiuderebbero subito il discorso con:
“Non è il caso di dire stupidaggini, il vento non soffia in quel modo”. Per questo il loro
lavoro è molto più facile. I tecnici di crittografia non hanno questo vantaggio: i sistemi
di sicurezza sono attaccati da persone intelligenti e con scopi malvagi, e dobbiamo considerare tutti i tipi di attacchi.
L’ambiente in cui operano i tecnici della sicurezza è ostile e molto difficile da vivere.
Si gioca senza regole, e il banco opera a nostro sfavore. Parliamo di un “attaccante” in
senso astratto, ma in realtà non sappiamo chi sia, che cosa sappia, quale sia il suo scopo,
quando attaccherà e di quali risorse disponga. Poiché l’attacco potrebbe verificarsi molto
tempo dopo che abbiamo progettato il sistema, l’attaccante ha il vantaggio del progresso
tecnologico a proprio favore. E con tutti questi vantaggi, deve soltanto trovare un unico
punto debole nel sistema, mentre noi dobbiamo proteggere tutti gli elementi. La nostra
missione è quella di realizzare un sistema in grado di opporsi a qualsiasi tentativo di
attacco, e così si crea uno squilibrio tra attaccante e difensore. Del resto, proprio per
questo motivo il mondo della crittografia è tanto interessante.

1.4 Paranoia professionale
Per lavorare nel campo della sicurezza, dovete assumere anche voi una mentalità un po’
deviata. Dovete pensare come un attaccante che cerca di trovare punti deboli nel vostro
sistema.Tutto ciò influenza anche il resto della vostra vita, come sa bene chiunque lavori
su sistemi di crittografia. Una volta che si comincia a pensare a come attaccare i sistemi,
si fa lo stesso per qualunque cosa. Improvvisamente ci si rende conto che è possibile
ingannare le persone vicine, e che loro possono fare lo stesso. Gli esperti di crittografia
sono affetti da paranoia professionale, ma è importante tenere questo aspetto separato

8   Capitolo 1

dalla vita reale, per non rischiare di impazzire. La maggior parte di noi riesce a mantenere
una certa sanità mentale... o almeno lo crede (ricordate: il fatto che voi non siate paranoici non significa che non ce ne siano altri intenzionati a ingannarvi o compromettere
il vostro sistema). In effetti, questa paranoia può anche essere divertente. Sviluppando
questa mentalità si arriva a osservare alcuni aspetti, dei sistemi e dell’ambiente, che la
maggior parte delle altre persone non nota.
Un atteggiamento paranoico è molto utile in questo lavoro. Supponete di lavorare su un
sistema di pagamento elettronico costituito da diversi elementi: il cliente, il commerciante,
la banca del cliente e quella del commerciante. Può essere molto difficile immaginare
quali siano le minacce, perciò facciamo ricorso al modello paranoico. Per ciascun utente,
supponiamo che tutti gli altri facciano parte di una grande cospirazione con lo scopo di
truffarlo; inoltre, supponiamo che l’attaccante potrebbe avere anche altri obiettivi, quali
violare la riservatezza delle transazioni dell’utente o impedirgli l’accesso al sistema in un
momento critico. Se il sistema crittografico riesce a sopravvivere al modello paranoico,
ha almeno una possibilità di combattere per la sopravvivenza nel mondo reale.
Nel seguito parleremo indifferentemente di paranoia professionale e di modello paranoico
per indicare la mentalità della sicurezza.

1.4.1 I vantaggi
Quando si è sviluppato un senso di paranoia professionale, non si osservano più i sistemi
nello stesso modo. Questa mentalità vi porterà beneficio in tutta la vostra carriera, poiché anche se non diventerete un esperto di crittografia, potrà capitarvi di lavorare alla
progettazione, implementazione o valutazione di un nuovo sistema software o hardware.
Se avete la mentalità della sicurezza, penserete costantemente a ciò che un attaccante
potrebbe tentare di fare e così sarete in grado di individuare prima i potenziali problemi
di sicurezza. Forse non sarete sempre in grado di rimediare da soli ai problemi rilevati,
ma il punto più importante è rendersi conto che un problema potrebbe esistere. Fatto
ciò, diventa relativamente semplice trovare altre persone che vi aiutino a risolverlo. Senza
la mentalità della sicurezza, tuttavia, rischiereste di non rendervi nemmeno conto che
il vostro sistema ha un problema di sicurezza e di conseguenza non sareste in grado di
proteggerlo.
Inoltre, le tecnologie cambiano molto rapidamente e un meccanismo di sicurezza valido oggi potrebbe risultare obsoleto tra 10 o 15 anni. Tuttavia, se imparate a pensare
alla sicurezza e a tenere conto della pericolosità degli avversari, porterete con voi tale
mentalità per il resto della vostra vita e l’applicherete anche alle tecnologie in continua
evoluzione.

1.4.2 Parlare degli attacchi
La paranoia professionale è uno strumento del mestiere fondamentale. Ogni volta che
incontrate un nuovo sistema, per prima cosa pensate a come sarebbe possibile violarlo.
Più presto trovate un punto debole, più presto imparerete a conoscere meglio il nuovo
sistema. Non c’è niente di peggio che lavorare per anni su un sistema e alla fine incontrare qualcuno che dice: “E se lo attaccassi in questo modo...?”. È terribile provare quel
momento di incertezza.

Il contesto della crittografia    9

In questo campo facciamo una distinzione molto netta tra attaccare l’opera di qualcuno e
attaccare la sua persona. Sul lavoro è un gioco alla pari. Se qualcuno propone qualcosa, è
un invito automatico ad attaccarlo. Se violate uno dei nostri sistemi, applaudiremo l’attacco
e lo faremo sapere a tutti (a seconda dell’attacco, potremmo semmai prenderci a calci
per non aver saputo trovare il punto debole, ma questo è un altro discorso). Cerchiamo
costantemente punti deboli in qualsiasi sistema, perché questo è l’unico modo per imparare
a realizzare sistemi più sicuri. Ecco un punto che dovrete imparare: un attacco alla vostra
opera non è un attacco contro di voi. Inoltre, quando attaccate un sistema, assicuratevi
sempre di criticare il sistema stesso, non i progettisti. Nel mondo della crittografia, come
in qualsiasi altro, gli attacchi personali portano a reazioni negative.
Tenete presente, tuttavia, che l’accettazione degli attacchi potrebbe non essere applicata
proprio da tutti, in particolare da persone che non hanno molta familiarità con il mondo
della crittografia e della sicurezza informatica. Chi non ha esperienza di sicurezza potrebbe
prendere la critica del proprio sistema come un attacco personale, con tutti i problemi
conseguenti. È importante, quindi, assumere un approccio diplomatico, anche se inizialmente è difficile far passare il messaggio. Stare troppo sul vago, dicendo qualcosa come:
“Potrebbe esserci qualche problema relativo alla sicurezza” potrebbe non bastare, dato
che si rischierebbe di ricevere risposte generiche come: “Ah sì, lo risolveremo” quando
in realtà c’è un problema strutturale. L’esperienza ci ha mostrato che il modo migliore
di farsi capire è quello di essere specifici dicendo qualcosa del tipo: “Se fate questo e
questo, allora un attaccante potrebbe fare questo”, ma una frase come questa potrebbe
risultare troppo aspra per il destinatario. Si potrebbe iniziare a chiedere:“Avete pensato a
ciò che potrebbe accadere se qualcuno facesse questo?” e così agevolare una discussione
sull’attacco con i progettisti del sistema. Potreste anche fare dei complimenti per i punti
di forza del sistema, notare quanto sia difficile realizzare sistemi sicuri e offrirvi di aiutare
a risolvere qualsiasi eventuale problema di sicurezza.
Così, la prossima volta che qualcuno attaccherà la sicurezza del vostro sistema, cercate
di non prenderla sul personale. Inoltre, quando voi attaccate un sistema, concentratevi
solo sugli aspetti tecnici, non criticate le persone che hanno lavorato duramente, e siate
sensibili al fatto che i progettisti potrebbero non essere avvezzi alla cultura della critica
costruttiva, tipica della comunità della sicurezza informatica.

1.5 Il modello delle minacce
Qualsiasi sistema può essere attaccato, non esiste la sicurezza perfetta. Un sistema di sicurezza serve a fornire accesso ad alcuni e non ad altri; alla fine è sempre necessario fidarsi
di qualcuno, e queste persone potrebbero essere in grado di attaccare il sistema.
È molto importante conoscere ciò che si tenta di proteggere e contro chi si vuole
proteggerlo. Quali sono i beni di valore? E le minacce? Sembrano domande semplici,
ma in realtà sono più difficili di quanto si pensi. Poiché non esiste la sicurezza perfetta,
quando diciamo che un sistema è “sicuro” intendiamo che fornisce un livello di sicurezza
sufficiente per i beni che ci interessa proteggere, contro determinate classi di minacce.
Dobbiamo valutare la sicurezza di un sistema nel modello delle minacce previsto.
La maggior parte delle aziende protegge la propria LAN con un firewall, ma molti degli
attacchi realmente pericolosi sono portati da personale interno e in questo caso il firewall
non offre alcuna protezione. Questo è uno sfasamento del modello delle minacce.

10   Capitolo 1

Un altro esempio è offerto da SET, un protocollo per lo shopping online con carta di
credito. Una delle sue caratteristiche è che esegue la cifratura del numero di carta di
credito in modo che nessuno in ascolto sulla connessione possa copiarlo. Questa è una
buona idea. Un’altra caratteristica è che nemmeno il commerciante può vedere il numero
di carta di credito, e qui c’è un problema.
La seconda caratteristica non funziona perché alcuni commercianti utilizzano il numero
di carta di credito per verificare la situazione del cliente o per applicare sovrapprezzi.
Interi sistemi commerciali sono stati progettati assumendo che il commerciante abbia
accesso al numero di carta di credito del cliente. E ora SET cerca di eliminare tale accesso.
Quando Niels Ferguson ha lavorato con SET, tempo fa, esisteva la possibilità di inviare
il numero di carta di credito due volte: una volta cifrato per la banca e una volta cifrato
per il commerciante, che in questo modo aveva la possibilità di vederlo (non abbiamo
verificato se questa opzione esiste ancora).
Tuttavia, anche con questa opzione a disposizione, SET non risolve interamente il problema. La maggior parte dei numeri di carta di credito sottratti non viene intercettata
durante il transito tra il cliente e il commerciante, ma direttamente dal database del
commerciante. SET protegge le informazioni soltanto mentre sono in transito.
Inoltre, SET presenta un altro difetto più grave. Molti anni fa la banca di Niels Ferguson in Olanda offriva una carta di credito compatibile con SET, puntando molto sulla
maggiore sicurezza degli acquisti online. Tuttavia, questo argomento si rivelò falso. È
abbastanza sicuro fare ordini online con una normale carta di credito, poiché il numero
di carta non è un segreto, lo si fornisce a qualsiasi commesso di negozio in cui si acquista
qualcosa. Il segreto vero è la firma del cliente. È quella che autorizza la transazione. Se
un commerciante si fa sottrarre il numero di carta di credito di un cliente, questi potrà
ricevere falsi addebiti, ma finché non vi è una firma scritta (o un codice PIN), non esiste l’indicazione di accettare la transazione e perciò manca la base legale per procedere
all’addebito. Nella maggior parte dei casi (dipende dalla legislazione valida), basta fare un
reclamo con le modalità previste per ottenere la restituzione del denaro. Naturalmente
è noioso richiedere una nuova carta di credito con un numero diverso, ma l’esposizione
al rischio dell’utente si limita a questo. Con SET, invece, le cose cambiano. Il protocollo
utilizza una firma digitale (ne parliamo nel Capitolo 12) dell’utente per autorizzare la
transazione. In questo modo la sicurezza è maggiore rispetto al semplice uso di un numero di carta di credito. Ma pensateci un attimo: ora l’utente è responsabile di qualsiasi
transazione eseguita dal software SET sul proprio PC. In questo modo la responsabilità
aumenta enormemente. Che succede se un virus infetta il PC dell’utente e viola il
software SET? Tale software potrebbe firmare transazioni fraudolente e causare danni
economici all’utente.
Dal punto di vista dell’utente, quindi, SET offre una sicurezza minore rispetto a una normale carta di credito. Le carte di credito normali sono sicure per lo shopping online, perché
l’utente può sempre ottenere un rimborso in caso di transazione fraudolenta, mentre
l’uso di SET aumenta l’esposizione al rischio. Perciò, benché il sistema di pagamento nel
suo complesso sia più sicuro, SET trasferisce il rischio residuo dal commerciante o dalla
banca all’utente. Così il modello delle minacce dell’utente cambia, passando da: “Subirò
un danno solo se falsificheranno la mia firma” a “Subirò un danno se falsificheranno la
mia firma o anche se un virus ben studiato infetterà il PC”.
I modelli delle minacce sono importanti. Ogni volta che iniziate a lavorare su un progetto
di crittografia, pensate bene a quali sono i beni da proteggere e le minacce da cui pro-

Il contesto della crittografia    11

teggerli. Un errore nell’analisi delle minacce può rendere inefficace un intero progetto.
In questo libro non parleremo molto di analisi delle minacce, poiché ci limitiamo alla
crittografia, ma per qualsiasi sistema reale non dovete mai dimenticare questa attività.

1.6 La crittografia non è la soluzione
La crittografia non è la soluzione dei vostri problemi di sicurezza. Potrà essere parte della
soluzione, o anche parte del problema. In alcuni casi, la crittografia inizialmente peggiora
il problema e non appare chiaro che il suo utilizzo possa migliorare le cose. Occorre
perciò considerare con attenzione l’utilizzo corretto della crittografia. Un esempio è
offerto dalla precedente discussione del protocollo SET.
Supponete di avere sul vostro computer un file segreto che non volete far leggere ad
altri. Potreste semplicemente proteggere il file system da accessi non autorizzati, oppure
cifrare il file e proteggere la chiave. Ora il file è cifrato, ma la natura umana è quella che è,
e sarete portati a proteggerlo meno bene. Forse lo memorizzerete su una chiavetta USB
senza fare troppo attenzione al rischio di perderla o di farsela rubare. Ma dove mettere la
chiave? Una buona chiave è troppo lunga da ricordare. Alcuni programmi memorizzano
la chiave sul disco dove si trovava il file, ma un attaccante in grado di accedere al file
segreto potrebbe anche recuperare la chiave, che gli consentirebbe di decifrarlo. Inoltre,
abbiamo introdotto un nuovo punto di attacco: se il sistema di cifratura non è sicuro, o
se il livello di casualità nella chiave è troppo basso, l’attaccante potrebbe violare lo stesso
sistema di cifratura. Alla fine, la sicurezza complessiva si è ridotta. Limitarsi a cifrare il
file, quindi, non è la soluzione. Potrebbe essere parte della soluzione, ma potrebbe anche
creare nuovi problemi da risolvere.
La crittografia può essere utilizzata per molti scopi. È una parte fondamentale di molti
sistemi di sicurezza, ma può anche rendere un sistema più debole se è utilizzata in maniera
inappropriata. In molti casi fornisce soltanto una sensazione di sicurezza, non reale. Si
sarebbe tentati di fermarsi qui, perché la maggior parte degli utenti vuole proprio questo:
sentirsi sicuro. Questo utilizzo della crittografia può anche consentire di soddisfare determinati standard e norme, anche se in realtà il sistema non è sicuro. In situazioni come
queste (troppo comuni), un amuleto qualsiasi in cui il cliente creda davvero fornirebbe
la stessa sensazione di sicurezza e avrebbe lo stesso effetto reale.

1.7 La crittografia è molto difficile
La crittografia è un campo complesso, in cui anche i più esperti progettano sistemi che
qualche anno dopo vengono violati. Si tratta di casi comuni che non destano sorpresa. La
proprietà dell’elemento più debole e l’ambiente ostile cospirano nel rendere il compito
dei tecnici di crittografia, e della sicurezza in generale, molto difficile.
Un altro problema significativo è la mancanza di test efficaci. Non esiste un modo noto
per testare se un sistema è sicuro. Nella comunità che si occupa di ricerca nella sicurezza
e nella crittografia, per esempio, si cerca di rendere pubblici i sistemi in modo che altri
esperti possano esaminarli, e le verifiche svolte in occasione di conferenze o dalle riviste
non sono sufficienti per individuare tutti i potenziali problemi di sicurezza prima della
pubblicazione di un sistema.Anche se molti occhi esperti esaminano un sistema, comunque, può darsi che esistano lacune che si manifestano soltanto dopo anni.

12   Capitolo 1

Esistono piccole aree della crittografia che gli esperti conoscono piuttosto bene. Questo
non significa che siano semplici, ma semplicemente che ormai ci si lavora da decenni
e gli aspetti critici sono probabilmente noti. Questo libro si occupa principalmente di
tali aree. Abbiamo cercato di raccogliere tutte le informazioni in nostro possesso sulla
progettazione e la realizzazione di sistemi crittografici.
Per qualche motivo, molti continuano a pensare che la crittografia sia facile. Non lo è.
Questo libro vi aiuterà a comprendere le sfide che si devono affrontare nella crittografia e a muovere i primi passi sulla strada per vincerle. Tuttavia, non mettetevi subito a
costruire un sistema di votazione cifrato o altri sistema in cui la sicurezza è un fattore
critico. Partite dalle basi e lavorate insieme ad altri, meglio se veri esperti di crittografia,
per progettare e analizzare un nuovo sistema. Anche noi, nonostante i molti anni di
esperienza nel campo, ci rivolgiamo ad altri esperti di crittografia e di sicurezza affinché
rivedano i sistemi che progettiamo.

1.8 La crittografia è la parte facile
La crittografia in sé è difficile, ma rimane una delle parti facili di un sistema di sicurezza.
Come una serratura, un componente crittografico ha limiti e requisiti ben definiti. Un
sistema di sicurezza nel suo complesso è molto più difficile da definire in modo chiaro,
perché è molto più variegato. Aspetti quali le procedure aziendali utilizzate per fornire
l’accesso e per verificare che le altre procedure siano effettivamente seguite sono molto
più difficili da affrontare, poiché la situazione è in continuo cambiamento. Un altro enorme problema della sicurezza informatica è la qualità del software. Il software di sicurezza
non può essere efficace se il software installato nella macchina presenta numerosi bug
che portano a falle nella sicurezza.
La crittografia è la parte facile, perché ci sono persone che sanno come svolgere un buon
lavoro. Ci sono esperti a cui rivolgersi per progettare un sistema crittografico. Non lavorano a buon mercato, e spesso non è facile avere a che fare con loro; insistono nel voler
cambiare altre parti del sistema per raggiungere il livello di sicurezza desiderato.Tuttavia,
per tutti gli scopi pratici, la crittografia pone problemi che sappiamo come risolvere, e
questo libro vi farà capire in che modo.
La parte rimanente del sistema di sicurezza include problemi che non sappiamo come
risolvere. La gestione e la memorizzazione della chiave è fondamentale per qualsiasi
sistema crittografico, ma la maggior parte dei computer non dispone di un luogo sicuro
per memorizzare una chiave. Un altro problema è la scarsa qualità del software, e un
altro ancora è la sicurezza della rete. Se aggiungiamo gli utenti, i problemi diventano
ancora più difficili.

1.9 Attacchi generici
È importante rendersi conto che alcuni problemi di sicurezza sono impossibili da risolvere.
Esistono attacchi generici, detti anche black box, contro alcuni tipi di sistemi. Un esempio
classico è quello della falla analogica per i sistemi DRM (Digital Rights Management); questi
sistemi cercano di controllare la copia di materiali digitali, come immagini, brani musicali,
film o libri. Tuttavia, nessuna tecnologia, crittografica o meno, è in grado di proteggere

Il contesto della crittografia    13

contro un attacco generico esterno al sistema. Per esempio, un attaccante potrebbe scattare
una foto di una schermata del computer per creare una copia di un’immagine, o utilizzare
un microfono per registrare un brano musicale mentre viene riprodotto.
È importante individuare quali sono gli attacchi generici contro un sistema, altrimenti
si rischia di perdere tempo per cercare di risolvere un problema irrisolvibile. Quando
qualcuno afferma di aver protetto un sistema contro un attacco generico, si impone un
notevole scetticismo.

1.10 La sicurezza e altri criteri di progettazione
La sicurezza non è mai l’unico criterio di progettazione per un sistema. Ne esistono
molti altri.

1.10.1 Sicurezza e prestazioni
Il Forth Bridge in Scozia è un ponte che ha dell’incredibile. Si tratta di una meraviglia
dell’ingegneria del diciannovesimo secolo, enormemente grande (e quindi costoso)
rispetto ai treni che lo attraverso. È incredibilmente sovradimensionato, ma i progettisti
hanno lavorato bene. Avevano a che fare con un problema che non era mai stato risolto
con successo in precedenza: costruire un grande ponte di acciaio. E ottennero un risultato
eccezionale. È stato un grande successo, il ponte è in uso ancora oggi oltre un secolo
dopo. Ecco che cosa significa buona ingegneria.
Negli anni, i progettisti di ponti hanno imparato a costruire strutture in modo molto meno
costoso e più efficiente, ma la prima priorità è sempre quella di realizzare un ponte che
sia sicuro e che funzioni. L’efficienza, o la riduzione dei costi, è un aspetto secondario.
Nel campo dei computer queste priorità sono state invertite. Il principale obiettivo del
progetto comprende troppo spesso requisiti di efficienza molto rigorosi. La prima priorità è sempre la velocità, anche in settori in cui non sarebbe realmente importante. Può
trattarsi della velocità del sistema, o della velocità con cui il sistema può essere portato
sul mercato.Tutto ciò porta a tagliare i costi legati alla sicurezza, e il risultato in generale
è un sistema sì efficiente, ma non abbastanza sicuro.
La storia del ponte Forth bridge non è finita. Nel 1878 Thomas Bouch completò quello
che all’epoca era il ponte più lungo del mondo, sopra il Firth of Tay presso Dundee.
Bouch utilizzò un nuovo progetto che metteva insieme ghisa e ferro battuto, e il ponte
fu considerato un capolavoro di ingegneria. La notte del 28 dicembre 1879, meno di
due anni dopo, il ponte crollò sotto un forte temporale, mentre era attraversato da un
treno con 75 passeggeri. Morirono tutti. Fu il più grande disastro dell’epoca, per quanto
riguarda le opere dell’ingegneria (William McGonagall scrisse un famoso poema sul
disastro del Tay Bridge, che terminava così Più forti costruiremo le nostre case/Meno probabilità
avremo di essere uccisi.Vale ancora oggi). Perciò, quando pochi anni dopo fu progettato il
Forth Bridge, i progettisti impiegarono molto più acciaio, non solo per rendere sicuro
il ponte, ma anche per farlo apparire sicuro agli occhi della gente.
Sappiamo tutti che gli ingegneri possono sbagliare, soprattutto quando fanno qualcosa
di nuovo. �����������������������������������������������������������������������������
E quando
���������������������������������������������������������������������������
sbagliano, possono succedere disastri. Gli ingegneri dell’epoca vittoriana ci hanno trasmesso una buona lezione: se sbagli, torna sui tuoi passi e procedi
con maggiore prudenza. Purtroppo il settore informatico ha dimenticato questa lezione.

14   Capitolo 1

Quando rileviamo importanti problemi di sicurezza nei nostri sistemi informatici, e capita
di frequente, spesso li accettiamo come se fossero dovuti al destino. Raramente torniamo al tavolo di lavoro e riprogettiamo un sistema in modo più prudente, ci limitiamo a
produrre qualche patch sperando che il problema si risolverà.
Ormai dovreste aver capito che tra sicurezza ed efficienza sceglieremo sempre la prima.
Quanto tempo della CPU siamo disposti a utilizzare per la sicurezza? Quasi tutto quello
disponibile. Non ci interessa se il 90% dei cicli della CPU sono spesi per un sistema di
sicurezza affidabile, ove l’alternativa sia quella di un sistema più veloce ma insicuro. La
mancanza di sicurezza è un problema gravissimo, per noi e per la maggior parte degli
utenti. Ecco perché molte persone sono costrette ancora a inviare pezzi di carta firmati
e devono continuamente preoccuparsi di virus e altri attacchi ai computer. I criminali
digitali del futuro saranno molto più preparati e disporranno di strumenti più avanzati,
perciò la sicurezza informatica sarà un problema sempre più importante. Siamo ancora
agli albori dell’era del crimine digitale. Dobbiamo proteggere i nostri computer molto
meglio.
Naturalmente esistono molti modi per ottenere la sicurezza, ma come Bruce Schneier
ha ben documentato in Secrets and Lies, una buona sicurezza è sempre una miscela di
prevenzione, rilevamento e risposta [114]. Il ruolo della crittografia sta principalmente
nella fase di prevenzione, che deve essere di livello molto alto per garantire che le fasi di
rilevamento e risposta (che possono e devono includere interventi manuali) non siano
troppo pesanti.Tuttavia, la crittografia può anche essere utilizzata per fornire meccanismi
di rilevamento più sicuri, come i log di audit a crittografia forte. Noi ci concentreremo
sul tema fondamentale di questo libro: la crittografia.
Il 90% dei cicli della CPU è una quota molto alta, ma la situazione non è così terribile.
Innanzitutto ricordate che siamo disposti a occupare il 90% della CPU per la sicurezza
se l’alternativa è un sistema non sicuro. Fortunatamente, in molti casi i costi della sicurezza possono essere nascosti all’utente. L’uomo è in grado di digitare circa 10 caratteri
al secondo e anche le macchine lente di un decennio fa non hanno problemi a stare
al passo. Le macchine di oggi sono oltre mille volte più veloci. Se utilizziamo il 90%
della CPU per la sicurezza, il computer apparirà dieci volte più lento: è circa la velocità
dei computer di cinque anni fa, che erano più che sufficienti per lavorare. Non sempre
dovremo utilizzare così tanti cicli della CPU per la sicurezza, ma il punto è che siamo
disposti a farlo.
Soltanto in poche situazioni i computer ci costringono ad attendere; accade per esempio
con le pagine web, la stampa di dati, l’avvio di alcuni programmi, l’accensione del computer e così via. Un buon sistema d sicurezza non dovrebbe rallentare alcuna di queste
attività. I computer moderni sono talmente veloci che è difficile immaginare come utilizzare i cicli della CPU in modo utile. Naturalmente possiamo eseguire l’alpha-blending
delle immagini sullo schermo, animazioni 3D o anche operazioni di riconoscimento
vocale, ma queste applicazioni, nelle parti legate all’elaborazione numerica, non svolgono
attività legate alla sicurezza, perciò non sarebbero rallentate da un sistema di sicurezza.
È il resto del sistema, che è già veloce più di quanto si possa immaginare in una scala
temporale umana, che subirà un carico di lavoro aggiuntivo. E non ci interessa se lavora
a una velocità dieci volte inferiore, se ciò aumenta la sicurezza. Nella maggior parte dei
casi non si noterà nemmeno il carico di lavoro in più, ma anche quando lo si noti, è il
costo della sicurezza.

Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

Il contesto della crittografia    15

Le nostre priorità sono sicurezza, sicurezza, sicurezza, mentre le prestazioni vengono
dopo. Naturalmente vogliamo che il sistema sia il più efficiente possibile, ma non a spese
della sicurezza. Capiamo che questa filosofia di progettazione non è sempre praticabile
nel mondo reale, talvolta infatti prevalgono le esigenze del mercato.
Raramente i sistemi vengono sviluppati partendo da zero, spesso la sicurezza viene
applicata per gradi, o anche dopo la messa in opera. È necessario mantenere la compatibilità con i sistemi esistenti, spesso non sicuri. Tutti gli autori di questo libro hanno
progettato molti sistemi di sicurezza sotto vincoli analoghi, e possono testimoniare che
è praticamente impossibile realizzare un buon sistema di sicurezza in questo modo. La
filosofia di progettazione di questo libro è “sicurezza prima e più di tutto”, e vorremmo
che fosse adottata maggiormente nei sistemi commerciali.

1.10.2 Sicurezza e funzionalità
La complessità è il peggior nemico della sicurezza e quasi sempre si presenta sotto forma
di funzionalità varie del sistema.
Immaginate un programma per computer con 20 opzioni diverse, ognuna delle quali
può essere attiva o disattiva. In totale si hanno più un milione di configurazioni diverse.
Per far funzionare il programma basta collaudare le combinazioni più comuni, ma per
renderlo sicuro è necessario esaminare ognuna delle configurazioni possibili, verificando
che sia sicura contro qualsiasi forma possibile di attacco. Il compito è impossibile. E la
maggior parte dei programmi ha ben più di 20 opzioni. Per realizzare qualcosa di sicuro,
la chiave è puntare alla semplicità.
Un sistema semplice non è necessariamente piccolo. Si possono costruire sistemi grandi
che rimangono abbastanza semplici. La complessità misura il numero di elementi che
interagiscono in un unico punto. Se un’opzione ha un effetto limitato a una piccola parte
del programma, non interagisce con un’altra il cui effetto è limitato a un’altra piccola
parte. Per creare un sistema grande e semplice è necessario fornire un’interfaccia molto
chiara e semplice tra diverse parti del sistema. I programmatori parlano di modularizzazione. Si tratta dei concetti di base di ingegneria del software. Una buona interfaccia,
semplice, isola il resto del sistema dai dettagli interni di un modulo, e questo dovrebbe
valere per qualsiasi opzione o funzionalità dei moduli.
In questo libro cerchiamo di definire interfacce semplici per primitive crittografiche.
Nessuna funzione, nessuna opzione, nessun caso speciale, nessuna particolarità da ricordare: soltanto la definizione più semplice che siamo riusciti a trovare. Alcune di queste
definizioni sono nuove, le abbiamo sviluppate proprio durante la scrittura del libro;
questo lavoro ci ha aiutato a dare forma al nostro pensiero riguardo i sistemi di sicurezza
di qualità, e speriamo che consenta anche a voi di fare altrettanto.

1.10.3 Sicurezza e sistemi in evoluzione
Un altro importante problema per la sicurezza è che i sistemi continuano la loro evoluzione anche dopo che sono stati messi in opera i meccanismi di sicurezza. Ciò significa che
il progettista del meccanismo di sicurezza non deve solo mostrare paranoia professionale
e considerare un’ampia varietà di attaccanti e di obiettivi di attacco, ma anche prevedere
i futuri utilizzi del sistema e prepararsi.Tutto ciò può comportare notevoli sfide, ed è un
aspetto che i progettisti di sistemi devono tenere bene a mente.

16   Capitolo 1

1.11 Letture consigliate
Chiunque si interessi alla crittografia dovrebbe leggere The Codebreakers di David Kahn
[67]. È una storia della crittografia, dall’antichità al ventesimo secolo, che fornisce molti
esempi dei problemi che si affrontano in questo campo. Un altro buon testo storico, e
di gradevole lettura, è The Code Book [120].
Per certi versi il libro che state leggendo è il seguito del primo libro di Bruce Schneier,
Applied Cryptography [112].Tale libro copre una più ampia varietà di argomenti e include
le specifiche di tutti gli algoritmi trattati, tuttavia non arriva a trattare i dettagli tecnici
di cui parliamo qui.
Per fatti e risultati precisi non esistono testi migliori di Handbook of Applied Cryptography,
di Menezes, van Oorschot e Vanstone [90]. Si tratta di un’enciclopedia della crittografia,
oltre che di un manuale di riferimento estremamente utile, ma come tutte le enciclopedie,
non è ideale per uno studio progressivo del campo.
Se siete interessati alla teoria della crittografia, potete consultare Foundations of Cryptography, di Goldreich [55, 56]. Un altro testo eccellente è Introduction to Modern Cryptography,
di Katz e Lindell [68]. Esistono anche molti appunti di corsi universitari disponibili
online, come quelli di Bellare e Rogaway [10].
Il precedente libro di Bruce Schneier, Secrets and Lies [114] offre una buona spiegazione
della sicurezza informatica in generale, e di come la crittografia rientri in un quadro
più ampio. Per quanto riguarda la security engineering non c’è testo migliore di quello di
Ross Anderson, Security Engineering [2]. Entrambi questi libri sono essenziali per capire
il contesto della crittografia.
Esistono molte buone risorse online per tenersi aggiornati sulle ultime novità della
crittografia e della sicurezza informatica. Vi suggeriamo di iscrivervi alla newsletter di
Bruce Schneier, Crypto-Gram, http://www.schneier.com/crypto-gram.html, e di leggere
il blog di Bruce, http://www.schneier.com/blog/.

1.12 Esercizi per la paranoia professionale
Si dice che uno dei modi migliori per apprendere una lingua straniera sia quello di immergersi in essa. Se volete imparare il francese, andate in Francia. Questo libro è studiato
in modo da farvi immergere nel linguaggio e nella mentalità della crittografia e della
sicurezza informatica. Gli esercizi che seguono vi aiuteranno a immergervi ancora di
più, obbligandovi a pensare alla sicurezza regolarmente, come quando leggete le notizie,
commentate gli ultimi accadimenti con gli amici o leggete la descrizione di un nuovo
prodotto su Slashdot, il noto sito di notizie tecnologiche. Pensare alla sicurezza non sarà
più un’attività limitata al momento in cui vi state occupando specificamente dell’argomento. Comincerete a pensare alla sicurezza anche mentre portate a spasso il cane, sotto
la doccia, al cinema. Svilupperete la paranoia professionale e inizierete a pensare come
un professionista della sicurezza.
Per chi sta facendo pratica nella sicurezza informatica (e in realtà per tutti gli informatici)
è estremamente importante essere consapevole del contesto più ampio che circonda la
tecnologia. Le tecnologie non sono mai isolate, ma costituiscono un piccolo aspetto
di un ecosistema più ampio costituito da persone, economia, etica, differenze culturali,

Il contesto della crittografia    17

politica, legge e così via. Gli esercizi seguenti vi offriranno l’opportunità di discutere ed
esaminare gli aspetti relativi al quadro d’insieme nelle loro relazioni con la sicurezza.
Suggeriamo di tornare regolarmente ad affrontare gli esercizi riportati di seguito. Cercate
di svolgerli più spesso possibile. Per esempio, potreste svolgerli ogni settimana per un
mese filato, oppure dopo aver terminato alcuni capitoli del libro. Inizialmente potrebbero
sembrarvi faticosi e noiosi, ma se vi ci mettete d’impegno, presto vi accorgerete che li
eseguirete automaticamente ogni volta che incontrerete una notizia legata al tema della
sicurezza, o un nuovo prodotto del settore. Questa è la paranoia professionale. Inoltre,
se continuate a svolgere questi esercizi durante la lettura del libro, noterete che la vostra
capacità di valutare le proprietà tecniche dei sistemi maturerà nel tempo.
Consigliamo, inoltre, di svolgere gli esercizi in compagnia di un amico o di un collega
di corso. Discutere i temi della sicurezza con altre persone può essere molto illuminante:
presto vi accorgerete che la sicurezza è un argomento incredibilmente sottile e che è
molto facile trascurare dei punti deboli di importanza critica.
Naturalmente, se non state seguendo un corso di gruppo, potete anche scegliere di
svolgere gli esercizi a mente, anziché elaborare relazioni scritte. Tuttavia, consigliamo
di scrivere una relazione almeno una volta, perché in questo modo vi costringerete a
pensare davvero in modo esaustivo a tutti gli aspetti da considerare.

1.12.1 Esercizi su eventi dell’attualità
Per questi esercizi dovrete analizzare in modo critico alcuni eventi che rientrano nell’attualità. L’evento scelto dovrebbe essere in qualche modo legato alla sicurezza informatica.
Forse meccanismi di sicurezza informatica migliori avrebbero ostacolato l’evento, o forse
l’evento spinge a progettare nuovi meccanismi o politiche di sicurezza.
La retrospettiva sugli eventi attuali dovrebbe essere breve, concisa, significativa e ben
scritta. Pensate di rivolgervi a un pubblico generico. L’obiettivo deve essere quello di
scrivere un articolo che aiuti il lettore a capire il campo della sicurezza informatica e il
suo ruolo in un contesto più ampio.
Dovete fornire una descrizione dell’evento, discutere perché si sia verificato, riflettere
su che cosa si sarebbe potuto fare diversamente prima che si verificasse (per prevenire,
impedire o modificare le conseguenze), descrivere gli aspetti più ampi in cui rientra (per
esempio aspetti etici o sociali) e proporre possibili risposte (per esempio come dovrebbe
rispondere la gente comune, i politici, le grandi aziende, i media o altri ancora).

1.12.2 Esercizi sulle revisioni della sicurezza
Questi esercizi puntano a sviluppare la mentalità della sicurezza nel contesto di prodotti o
sistemi reali. Le revisioni della sicurezza hanno l’obiettivo di valutare i potenziali problemi
di sicurezza e riservatezza di nuove tecnologie, determinarne la gravità e discutere come
risolverli. Queste revisioni dovrebbero esaminare in modo approfondito la tecnologia
discussa, e quindi essere decisamente più lunghe rispetto alle relazioni degli esercizi su
eventi dell’attualità.

18   Capitolo 1

Ogni revisione della sicurezza dovrebbe contenere i seguenti elementi.
• Riepilogo della tecnologia in esame. Potete scegliere di valutare un prodotto specifico
(per esempio una pompa impiantabile wireless per la somministrazione di farmaci)
o una classe di prodotti che abbiano uno scopo comune (come l’insieme di tutti i
dispositivi medicali impiantabili). Il riepilogo dovrebbe mantenersi su un livello alto
ed essere lungo uno o due paragrafi. Definite gli aspetti della tecnologia importanti
per le osservazioni nei punti che seguono.
Per questi esercizi è accettabile fare delle ipotesi sul funzionamento dei prodotti.
Tuttavia, in questo caso dovete chiarirlo e descrivere esplicitamente quali siano tali
ipotesi.
Essere in grado di descrivere in breve un prodotto (anche con ipotesi esplicitamente
presentate) è molto importante. Se non conoscete la tecnologia a un livello che vi
consenta di produrre un riepilogo chiaro ed efficace, probabilmente non la conoscete
abbastanza bene per poterne valutare la sicurezza e la riservatezza.
• Definite almeno due beni da proteggere e, per ciascuno, un obiettivo di sicurezza
corrispondente. Spiegate perché gli obiettivi sono importanti. Dovreste scrivere una
o due frasi per bene/obiettivo.
• Definite almeno due possibili minacce, definite come azioni svolte da un avversario
che vuole compromettere un bene. Fornite un avversario di esempio per ciascuna
minaccia. Dovreste scrivere una o due frasi per minaccia/avversario.
• Definite almeno due potenziali punti deboli. Anche in questo caso argomentate
scrivendo una o due frasi per ciascuno. Per gli scopi di questi esercizi non è necessario verificare in modo esaustivo se questi punti deboli non siano solo potenziali
ma effettivi.
• Definite potenziali difese. Descrivete le difese che il sistema potrebbe usare o forse
usa già per affrontare i potenziali punti deboli individuati al punto precedente.
• Valutate i rischi associati ai beni, alle minacce e ai punti deboli descritti. A livello
informale, che gravità stimate per queste combinazioni di beni, minacce e potenziali
punti deboli?
• Conclusioni. Scrivete alcune significative riflessioni sui punti precedenti e discutete
anche aspetti relativi al quadro più ampio (etica, probabilità di evoluzione della
tecnologia e così via).
Potete trovare alcuni esempi di revisioni della sicurezza online presso http://www.
schneier.com/ce.html.

1.13 Esercizi generali
Esercizio 1.1 Create un attack tree per rubare un’auto. Per questo e gli altri esercizi
con attack tree, potete presentare l’albero in forma di figura (Figura 1.1) oppure come
elenco numerato e strutturato (1, 1.1, 1.2, 1.2.1, 1.2.2, 1.3, ... ).
Esercizio 1.2 Create un attack tree per entrare in una palestra senza pagare.
Esercizio 1.3 Create un attack tree per mangiare a un ristorante senza pagare.

Il contesto della crittografia    19

Esercizio 1.4 Create un attack tree per determinare il nome utente e la password del
conto bancario di una persona.
Esercizio 1.5 Create un attack tree per leggere la posta di una persona.
Esercizio 1.6 Create un attack tree per impedire a una persona di leggere la propria
email.
Esercizio 1.7 Create un attack tree per inviare email assumendo l’identità di un’altra
persona. In questo caso lo scopo dell’attaccante è quello di convincere il destinatario
che un’email è stata inviata da un’altra persona (per esempio Bob), mentre in realtà Bob
non ha mai inviato tale email.
Esercizio 1.8 Trovate un nuovo prodotto o sistema che è stato annunciato o rilasciato
negli ultimi tre mesi. Eseguite una revisione della sicurezza per tale prodotto o sistema
come descritto nel Paragrafo 1.12. Scegliete uno dei beni individuati e costruite un
attack tree per comprometterlo.
Esercizio 1.9 Fornite un esempio concreto, scelto dai media o dalle vostre esperienze
personali, in cui degli attaccanti hanno compromesso un sistema sfruttando un elemento
diverso da quello più debole. Descrivete il sistema, spiegate qual è secondo voi il suo
elemento più debole e perché, e spiegate in che modo il sistema è stato compromesso.
Esercizio 1.10 Fornite un esempio concreto, esclusi quelli riportati in questo capitolo,
in cui migliorando la sicurezza di un sistema contro un certo tipo di attacco si rischia
di aumentare la probabilità di altri attacchi.

Capitolo 2

Introduzione
alla crittografia
In questo capitolo sono introdotti i concetti di base
della crittografia e presentate informazioni preliminari necessarie per proseguire nella lettura del libro.

Sommario
2.1 Cifratura
2.2 Autenticazione

2.1 Cifratura

2.3 Cifratura a chiave
pubblica

La cifratura è l’obiettivo di partenza della crittografia.
Lo schema generale è mostrato nella Figura 2.1.Alice
e Bob vogliono comunicare tra loro (l’uso di nomi
di persona, in particolare Alice, Bob ed Eve, è una
tradizione nel mondo della crittografia). Tuttavia, in
generale i canali di comunicazione devono essere
considerati non sicuri. Eve sta facendo attività di
eavesdropping, intercettazione, sul canale. Ogni messaggio m che Alice invia a Bob viene ricevuto anche
da Eve (lo stesso accade per i messaggi inviati da Bob
verso Alice, ma si tratta dello stesso problema a parti
inverse. Se possiamo proteggere i messaggi di Alice,
la stessa soluzione funzionerà anche per i messaggi di
Bob, quindi ci concentriamo sui messaggi di Alice).
In che modo Alice e Bob possono comunicare senza
che Eve intercetti tutto?

2.4 Firme digitali
2.5 Infrastruttura a chiave
pubblica: PKI
2.6 Gli attacchi
2.7 Entriamo nei dettagli
2.8 Livelli di sicurezza
2.9 Prestazioni
2.10 Complessità
2.11 Esercizi

22   Capitolo 2

Eve
m
Alice
m

Bob
m

m

Figura 2.1 In che modo Alice e Bob possono comunicare in maniera sicura?

Per evitare che Eve possa venire a conoscenza del contenuto della conversazione tra Alice
e Bob, questi ultimi utilizzano la crittografia come mostrato nella Figura 2.2. In primo
luogo Alice e Bob si mettono d’accordo su una chiave segreta Ke. Questa operazione va
svolta attraverso un canale di comunicazione che Eve non sia in grado di intercettare.
Alice potrebbe inviare via email una copia della chiave a Bob, o qualcosa del genere.
Torneremo a parlare di scambio delle chiavi più avanti.
Eve
c
Alice
m, c := E(Ke , m)

Bob
c

c, m := D(Ke , c)

Figura 2.2 Configurazione generica per la cifratura.

Quando Alice vuole inviare un messaggio m, per prima cosa lo cifra con una funzione di
crittografia. Scriviamo la funzione di cifratura come E(Ke, m) e indichiamo il risultato come
testo cifrato c (il messaggio originale è chiamato testo in chiaro). Invece di inviare m a Bob,
Alice invia il testo cifrato c := E(Ke, m). Quando Bob riceve c, può decifrarlo utilizzando
la funzione di decifratura D(Ke, c) per ottenere il testo in chiaro originale m che Alice
voleva inviargli. Ma Eve non conosce la chiave Ke e quando riceve il testo cifrato c, non
è in grado di decifrarlo. Una buona funzione di cifratura impedisce di ottenere il testo
in chiaro m a partire dal testo cifrato c senza conoscere la chiave. O meglio, una buona
funzione di cifratura dovrebbe fornire una riservatezza ancora maggiore: un attaccante
non dovrebbe essere in grado di ottenere alcuna informazione sul messaggio m, a parte
eventualmente la sua lunghezza e l’ora in cui è stato inviato.
Questo scenario ha applicazioni ovvie per la trasmissione di email, ma si applica anche
all’attività di memorizzazione dei dati, che può essere pensata come trasmissione di
messaggi nel tempo, anziché nello spazio. In quel caso, infatti, spesso Alice e Bob sono
la stessa persona ma in momenti diversi.

2.1.1 Il principio di Kerckhoff
Per decifrare il testo cifrato, Bob deve conoscere l’algoritmo di decifratura D e la chiave Ke. Una regola importante è il principio di Kerckhoff: la sicurezza dello schema

Introduzione alla crittografia    23

di cifratura deve dipendere solo dalla segretezza della chiave Ke, non dalla segretezza
dell’algoritmo.
Questa regola è fondata su molte e ottime motivazioni. Gli algoritmi sono difficili da
cambiare. Sono integrati in software o hardware, difficili da aggiornare. In situazioni
pratiche, lo stesso algoritmo viene utilizzato per molto tempo. Inoltre, se mantenere
segreta una semplice chiave è difficile, mantenere segreto l’algoritmo lo è ancora di più
difficile (e di conseguenza aumentano i costi). Nessuno costruisce un sistema crittografico
per due soli utenti. Ogni partecipante al sistema (e potrebbero essere milioni) utilizza
lo stesso algoritmo; a Eve basterebbe ottenere l’algoritmo da uno di essi, e uno facile da
attaccare generalmente si trova. Eve potrebbe semplicemente rubare un portatile su cui
sia disponibile l’algoritmo. Inoltre, ricordate il nostro modello basato sulla paranoia? Eve
potrebbe anche essere uno degli utenti del sistema, persino uno dei suoi progettisti.
Esistono buone ragioni per rendere pubblici i gli algoritmi. Per esperienza sappiamo
che è molto facile commettere un piccolo errore e creare un algoritmo di crittografia
debole. Se l’algoritmo non è pubblico, nessuno troverà la falla finché un attaccante non
provi ad attaccarlo, ma in questo caso l’attaccante potrebbe utilizzare la falla per entrare
nel sistema. Abbiamo analizzato una certa quantità di algoritmi di crittografia segreti e
tutti avevano uno o più punti deboli. Ecco perché c’è una salutare sfiducia verso gli argomenti proprietari, riservati o comunque segreti. Non lasciatevi ingannare dalle vecchie
abitudini:“Se teniamo segreto anche l’algoritmo, aumentiamo la sicurezza”. È sbagliato. Il
potenziale aumento di sicurezza è ridotto, mentre la potenziale diminuzione di sicurezza
è enorme. La lezione è semplice: non fidatevi degli algoritmi segreti.

2.2 Autenticazione
Alice e Bob hanno un altro problema nella Figura 2.1: non solo Eve può intercettare il
messaggio, può anche modificarlo. Questo richiede che Eve abbia un controllo appena
maggiore sul canale di comunicazione, ma non è affatto impossibile che accada. Per
esempio, nella Figura 2.3 Alice tenta di inviare il messaggio m, ma Eve interferisce con
il canale di comunicazione. Invece di ricevere m, Bob riceve un differente messaggio m'.
Supponiamo che Eve venga a conoscenza del contenuto del messaggio m che Alice ha
tentato di inviare; allora potrebbe fare anche altre cose, come cancellare un messaggio
in modo che Bob non lo riceva mai, inserire un nuovo messaggio inventato da lei, registrare un messaggio e inviarlo a Bob in un momento successivo, o modificare l’ordine
dei messaggi.
Eve
m’
Alice
m

Bob
m

m’

Figura 2.3 Come fa Bob a sapere chi ha inviato il messaggio?

m’

24   Capitolo 2

Considerate il momento in cui Bob ha appena ricevuto un messaggio. Perché dovrebbe
credere che il messaggio provenga da Alice? Non ha ragione di credere che sia così. E se
non sa chi ha inviato il messaggio, allora quest’ultimo è pressoché inutile.
Per risolvere il problema, introduciamo il concetto di autenticazione. Come la crittografia,
l’autenticazione utilizza una chiave segreta nota sia a Bob sia ad Alice. Chiameremo la
chiave di autenticazione Ka per distinguerla dalla chiave di crittografia Ke. La Figura 2.4
mostra il processo di autenticazione di un messaggio m. Quando Alice invia il messaggio
m, calcola un codice di autenticazione del messaggio, MAC (Message Authentication Code).
Alice calcola il MAC a come a := h(Ka, m), dove h è la funzione MAC e Ka è la chiave
di autenticazione. Ora Alice invia m e a a Bob. Quando Bob riceve m e a, calcola a sua
volta il valore che a dovrebbe avere, utilizzando la chiave Ka, e lo confronta con il valore
di a ricevuto.
Eve
m’
Alice
m, a := h (Ka , m)

Bob
m, a

m, a

?
m, a = h (Ka , m)

Figura 2.4 Configurazione generica per l’autenticazione.

Ora Eve vuole modificare il messaggio m in un diverso messaggio m'. Se si limitasse a
sostituire m con m', Bob calcolerebbe h(Ka, m') e confronterebbe il risultato a. Ma una
buona funzione MAC non fornisce lo stesso risultato per messaggi diversi, quindi Bob
si accorgerebbe che il messaggio non è corretto. Dato che il messaggio è sbagliato, in
un modo o nell’altro, Bob lo scarterà.
Se supponiamo che Eve non conosca la chiave di autenticazione Ka, il solo modo in cui
potrebbe ottenere un messaggio e un MAC valido è quello di intercettare Alice mentre
invia il messaggio a Bob. Ciò permette ancora a Eve di provare a commettere qualche
malefatta. Eve può registrare i messaggi e i loro MAC, e quindi riproporli inviandoli di
nuovo a Bob in qualsiasi momento successivo.
L’autenticazione pura offre solo una soluzione parziale. Eve può ancora cancellare i
messaggi inviati da Alice, può anche riproporre vecchi messaggi o modificarne l’ordine.
Di conseguenza, l’autenticazione è quasi sempre combinata con uno schema di numerazione per numerare i messaggi in sequenza. Se m contiene tale numero sequenziale, Bob
non si lascia ingannare dai vecchi messaggi riproposti da Eve: vedrà semplicemente che
il messaggio ha un MAC corretto, ma un numero sequenziale relativo a un messaggio
vecchio, quindi lo scarterà.
L’autenticazione in combinazione con la numerazione dei messaggi risolve la maggior
parte dei problemi. Eve può ancora impedire la comunicazione tra Alice e Bob, o ritardare
i messaggi eliminandoli e inviandoli a Bob in un momento successivo. Se i messaggi
non sono cifrati, Eve può cancellarli o ritardarli selettivamente in base al loro contenuto,
ma non può fare altro.
Il miglior modo di affrontare il problema consiste nel considerare il caso in cui Alice
invia una sequenza di messaggi m1, m2, m3, ... e Bob accetta solo i messaggi con un MAC

Introduzione alla crittografia    25

valido e per i quali il numero di sequenza è strettamente maggiore del numero di sequenza dell’ultimo messaggio accettato (strettamente maggiore significa “maggiore e non
uguale a”). Quindi Bob riceve una sequenza di messaggi che è una sottosequenza della
sequenza inviata da Alice. Una sottosequenza è semplicemente la sequenza di partenza
da cui mancano zero o più messaggi.
Questo è il massimo aiuto che la crittografia può fornire in questa situazione. Bob riceverà una sottosequenza dei messaggi inviati da Alice, ma Eve può soltanto eliminare
alcuni messaggi o fermare tutte le comunicazioni, non può manipolare il contenuto
dei messaggi. Per evitare la perdita di informazioni, Alice e Bob utilizzeranno spesso
uno schema che prevede il rinvio dei messaggi andati persi, ma questo è un aspetto più
specifico dell’applicazione e non rientra nella crittografia.
Naturalmente, in molte situazioni Alice e Bob dovranno utilizzare sia la crittografia,
sia l’autenticazione. Discuteremo in dettaglio questa combinazione più avanti. Non
confondete mai i due concetti: la cifratura di un messaggio non garantisce contro la
manipolazione del suo contenuto, e l’autenticazione di un messaggio non ne mantiene
la segretezza. Uno degli errori classici nel campo della crittografia è quello di pensare
che cifrare un messaggio garantisca anche che un attaccante non lo possa modificare.
Non è così.

2.3 Cifratura a chiave pubblica
Per utilizzare la crittografia come discusso nel Paragrafo 2.1, Alice e Bob devono condividere la chiave Ke. E come farlo? Alice non può semplicemente inviare la chiave a Bob
utilizzando il canale di comunicazione, poiché anche Eve potrebbe leggerla. Il problema
della distribuzione e della gestione delle chiavi è uno dei più difficili da affrontare, e sono
disponibili solo soluzioni parziali.
Alice e Bob potrebbero aver scambiato la chiave il mese scorso quando si sono incontrati
per un aperitivo. Ma se Alice e Bob fanno parte di un gruppo di 20 amici a cui piace
comunicare l’uno con l’altro, ogni membro del gruppo dovrà scambiare un totale di 19
chiavi. In totale, il gruppo deve scambiare 190 chiavi. La situazione è già molto complessa,
e peggiora all’aumentare del numero di persone con cui Alice comunica.
Lo scambio di chiavi crittografiche è un problema antico, e un contributo importante
alla soluzione è portato dalla crittografia a chiave pubblica. Discuteremo per prima cosa
la cifratura a chiave pubblica, rappresentata nella Figura 2.5. Abbiamo lasciato Eve fuori
dal diagramma; da adesso in avanti, supponiamo che tutte le comunicazioni siano sempre
accessibili a un nemico come Eve. A parte l’assenza di Eve, la figura è molto simile alla
Figura 2.2; la differenza sostanziale è che Alice e Bob non usano più la stessa chiave,
ma chiavi diverse. Questa è l’idea che sta alla base della crittografia a chiave pubblica: la
chiave usata per cifrare un messaggio è diversa da quella usata per decifrarlo.
Alice
m, c := E(PBob, m)

Bob
c

c, m := D(S Bob, c)

Figura 2.5 Configurazione generica per la cifratura a chiave pubblica.

26   Capitolo 2

Per prima cosa Bob genera una coppia di chiavi (SBob, PBob) utilizzando uno speciale
algoritmo. Le due chiavi sono la chiave segreta SBob e la chiave pubblica PBob. Poi Bob
fa un cosa a prima vista sorprendente: rende nota PBob come sua chiave pubblica. Questa
operazione rende la chiave pubblica di Bob, PBob, accessibile a chiunque, incluse Alice
ed Eve (perché dovrebbe chiamarsi chiave pubblica, altrimenti?).
Quando Alice vuole inviare un messaggio a Bob, per prima cosa si procura la chiave
pubblica di Bob. Potrebbe ottenerla da un elenco pubblico, o forse da qualcuno di cui si
fida. Alice cifra il messaggio m con la chiave pubblica PBob per ottenere il testo cifrato c,
che invia a Bob. Questi utilizza la propria chiave segreta SBob e l’algoritmo crittografico
per decifrare il messaggio c e ottenere il messaggio m.
Affinché tutto funzioni, l’algoritmo di generazione della coppia di chiavi, l’algoritmo di
cifratura e di decifratura devono essere realizzati in modo tale di assicurare che la decifratura
restituisca il messaggio originale. In altre parole, la relazione D(SBob, E(PBob, m)) = m deve
essere valida per ogni possibile messaggio m. Esamineremo nel dettaglio questo aspetto più
avanti.
Non solo le due chiavi che Alice e Bob usano sono diverse, ma anche gli algoritmi di
cifratura e decifratura possono essere molto differenti. Tutti gli schemi di crittografia a
chiave pubblica si affidano pesantemente alla matematica. Un requisito ovvio è che non
deve essere possibile ottenere la chiave segreta dalla corrispondente chiave pubblica, ma
ci sono anche molti altri requisiti.
Questo tipo di cifratura è chiamato cifratura a chiave asimmetrica, o a chiave pubblica, in
contrasto con la cifratura a chiave simmetrica o a chiave segreta di cui abbiamo parlato
in precedenza.
La crittografia a chiave pubblica semplifica notevolmente il problema della distribuzione
delle chiavi. Per utilizzare questo sistema, Bob deve semplicemente distribuire una singola
chiave pubblica che chiunque può utilizzare. Alice pubblica la propria chiave pubblica
nello stesso modo; ora Alice e Bob possono comunicare in maniera sicura. Anche nel
caso di gruppi ampi, ogni membro del gruppo deve solo pubblicare una singola chiave
pubblica, cosa che risulta gestibile con relativa facilità.
E allora perché mai utilizzare la cifratura a chiave segreta, se la cifratura a chiave pubblica
è molto più facile? Perché la cifratura a chiave pubblica è molto meno efficiente, di diversi
ordini di grandezza. Utilizzarla in ogni situazione è semplicemente troppo costoso. In
sistemi reali che utilizzano la crittografia a chiave pubblica, quasi sempre si può notare
che si tratta di una miscela di algoritmi a chiave pubblica e a chiave segreta. Gli algoritmi
a chiave pubblica sono utilizzati per designare una chiave segreta, che viene poi utilizzata
per cifrare i dati effettivi. Questo approccio combina la flessibilità della crittografia a
chiave pubblica con l’efficienza della crittografia a chiave segreta.

2.4 Firme digitali
Le firme digitali sono equivalenti alle chiavi pubbliche dei codici di autenticazione dei
messaggi. La situazione generica è mostrata nella Figura 2.6. Questa volta è Alice che usa
un algoritmo per generare una coppia di chiavi (SAlice, PAlice) e pubblica la chiave pubblica
PAlice. Quando Alice vuole inviare un messaggio firmato m a Bob, elabora una firma
s := σ(SAlice, m) e invia m e s a Bob. Questi utilizza un algoritmo di verifica v(PAlice, m, s)
che sfrutta la chiave pubblica di Alice per verificare la firma. La firma funziona proprio

Introduzione alla crittografia    27

come un MAC, a parte il fatto che Bob può verificarla con la chiave pubblica, mentre
la chiave segreta è necessaria per creare una nuova firma.
Alice
m, s := V(SAlice, m)

Bob
m, s

m, X (PAlice, m, s)?

Figura 2.6 Configurazione generica per la firma digitale.

Bob necessita soltanto della chiave pubblica di Alice per verificare che il messaggio
provenga da lei. È interessante notare che chiunque può conoscere la chiave pubblica di
Alice e verificare che il messaggio provenga da lei. Ecco perché chiamiamo s una firma
digitale. In un certo senso, Alice firma il messaggio. Qualora vi fosse un contenzioso,
Bob potrebbe presentare il messaggio m e la firma s a un giudice e provare che Alice ha
firmato il messaggio.
Tutto ciò è molto bello in teoria, e funziona... in teoria. Nel mondo reale le firme digitali
hanno molte limitazioni di cui è importante rendersi conto. Il problema principale è
che Alice non elabora da sé una firma, è il suo computer a calcolarla. La firma digitale,
di conseguenza, non è una prova che Alice abbia approvato il messaggio, o che lo abbia
almeno visto sullo schermo del suo computer. Data la facilità con cui i virus prendono il
controllo dei computer, la firma digitale in realtà prova davvero poco in questo scenario.
Nondimeno, quando vengono utilizzate in maniera appropriata, le firme digitali sono
estremamente utili.

2.5 Infrastruttura a chiave pubblica: PKI
La crittografia a chiave pubblica semplifica la gestione delle chiavi, tuttavia Alice deve
ancora trovare la chiave pubblica di Bob. Come può essere sicura che sia davvero la chiave
di Bob e non quella di qualcun altro? Forse Eve ha creato una coppia di chiavi e ne ha
pubblica una facendo apparire che sia stato Bob a farlo.
La soluzione generale è quella di utilizzare una infrastruttura a chiave pubblica o PKI (Public
Key Infrastructure).
Alla base vi è un ente centrale chiamato autorità di certificazione, o CA (Certification Authority). Ogni utente presenta la propria chiave pubblica alla CA e si identifica con essa. La
CA quindi firma la chiave pubblica dell’utente utilizzando una firma digitale. Il messaggio
firmato, o certificato, recita:“Io, la CA, ho verificato che la chiave pubblica PBob appartiene
a Bob”. Spesso il certificato include una data di scadenza e altre informazioni utili.
Utilizzando i certificati, è molto più semplice per Alice trovare la chiave di Bob. Assumeremo che Alice abbia la chiave pubblica della CA e abbia verificato che sia la chiave
corretta. Alice può quindi recuperare la chiave di Bob da un database, oppure Bob può
inviare la sua chiave ad Alice. Alice può verificare il certificato in riferimento alla chiave,
utilizzando la chiave della CA, già in suo possesso. Questo certificato le assicura di avere
la chiave pubblica corretta per comunicare con Bob. Analogamente, Bob può cercare la
chiave pubblica di Alice e assicurarsi di comunicare con la persona giusta.

28   Capitolo 2

In una PKI, ogni partecipante deve soltanto fare in modo che la CA certifichi la propria
chiave pubblica, e conoscere la chiave pubblica della CA per poter verificare i certificati
degli altri partecipanti. È uno scenario decisamente più agevole rispetto a quello in cui
si devono scambiare chiavi con tutte le persone con cui vuole comunicare. Questo è il
grande vantaggio di una PKI: ci si registra una sola volta e si utilizza ovunque.
Per ragioni pratiche, una PKI è spesso impostata con diversi livelli di CA. C’è una CA
di primo livello, chiamata root, che emette i certificati sulle chiavi delle CA di livello
inferiore, che invece certificano le chiavi degli utenti. Il sistema continua a comportarsi
nello stesso modo, ma ora Alice deve controllare due certificati per verificare la chiave
di Bob.
Una PKI non è la soluzione definitiva: ci sono ancora molti problemi. In primo luogo, la
CA deve avere la fiducia di tutti. In alcune situazioni, non è un problema. All’interno di
una società, il reparto del personale conosce tutti i dipendenti e può ricoprire il ruolo di
CA. Ma nel mondo non esiste un ente che sia considerato affidabile da tutti. L’idea che
una singola PKI possa gestire tutti gli utenti del mondo non sembra praticabile.
Il secondo problema è quello della responsabilità. Che cosa succede se la CA fornisce
un certificato falso, o se la chiave privata della CA viene rubata? Alice si fiderebbe di un
certificato falso, e potrebbe perdere molto denaro a causa di ciò. Chi paga? La CA può
farsi carico di questo genere di assicurazione? Ciò richiede una relazione d’affari molto
più estesa tra Alice e la CA.
Ci sono attualmente molte società che stanno cercando di diventare la CA leader nel
mondo.VeriSign è probabilmente la più conosciuta.Tuttavia,VeriSign limita esplicitamente
la propria responsabilità nel caso in cui non possa essere in grado di assolvere alla propria
funzione. Nella maggior parte dei casi la responsabilità è limitata a 100 dollari statunitensi, meno di quanto molti utenti pagano per una normale transazione di commercio
elettronico: transazioni che sono rese sicure utilizzando certificati firmati da VeriSign. In
questi casi non ci sono problemi perché un pagamento con carta di credito è sicuro per
il consumatore, tuttavia, non compreremo la nostra auto nuova utilizzando un certificato
che VeriSign garantisce solo per 100 dollari.

2.6 Gli attacchi
Dopo aver descritto le più importanti funzioni utilizzate in crittografia, parleremo ora
di alcuni attacchi, concentrandoci su quelli indirizzati contro gli schemi di cifratura.
Esistono molti tipi di attacchi, ognuno dei quali ha la sua specifica gravità.

2.6.1 Il modello con solo testo cifrato
L’attacco con solo testo cifrato (ciphertext-only) è quello che molti intendono quando si parla
di entrare in un sistema di cifratura. Questa è la situazione in cui Alice e Bob eseguono
la cifratura dei loro dati e tutto ciò che un attaccante può vedere è il testo cifrato. Il
tentativo di decifrare un messaggio quando si conosce solo il testo cifrato è chiamato
attacco con solo testo cifrato. È il tipo di attacco più difficile, poiché l’attaccante ha a
disposizione la quantità di informazione minima.

Introduzione alla crittografia    29

2.6.2 Il modello con testo in chiaro noto
Nell’attacco con testo in chiaro noto (known-plaintext) l’attaccante conosce sia il testo cifrato,
sia il testo in chiaro. L’obiettivo più ovvio è quello di trovare la chiave di decifratura.
Apparentemente sembra uno scenario poco plausibile: come si potrebbe conoscere il
testo in chiaro? In realtà esistono molte situazioni nelle quali un attaccante può venire a
conoscenza del testo in chiaro di una comunicazione. A volte ci sono messaggi che sono
facilmente prevedibili; per esempio quando Alice è in vacanza e ha un risponditore email
automatico che invia una risposta: “Sono in vacanza” a ogni email in arrivo. È possibile
ottenere una esatta copia di questo messaggio inviando una email ad Alice e leggendo
la risposta. Quando Bob invia un messaggio email ad Alice, il risponditore automatico
risponde, questa volta cifrando il messaggio. Ora è disponibile sia il messaggio in chiaro,
sia il messaggio cifrato. Se l’attaccante riesce a trovare la chiave, potrà decifrare tutti gli
altri messaggi che Alice e Bob scambiano con la stessa chiave. L’ultima parte è importante
e vale la pena ripeterla: si utilizza la conoscenza di alcune coppie di testo in chiaro e testo
cifrato per ottenere la chiave, e successivamente si sfrutta la conoscenza della chiave per
decifrare altri testi cifrati.
Un’altra situazione tipica è quella in cui Alice invia lo stesso messaggio a molte persone
tra cui l’attaccante. Questo rende disponibile all’attaccante sia il testo in chiaro, sia i testi
cifrati della copia che Alice ha inviato a tutti gli altri.
Supponiamo che Alice e Bob si scambino le bozze di un articolo di stampa. Una volta
che l’articolo viene pubblicato, l’attaccante può conoscere il testo in chiaro e il testo
cifrato.
Anche se non conosce l’intero testo cifrato, spesso l’attaccante ne conosce una parte.
Le email hanno un inizio prevedibile, o una firma fissa alla fine. L’intestazione di un
pacchetto IP è altamente prevedibile. Tali dati prevedibili portano a avere un testo in
chiaro parzialmente noto. Un attacco con testo in chiaro noto è molto più potente di
un attacco con solo testo cifrato, poiché l’attaccante dispone di più informazioni rispetto
al caso del solo testo cifrato.

2.6.3 Il modello con testo in chiaro scelto
Il livello successivo è quello in cui l’attaccante può scegliere il testo in chiaro. Questo è
un tipo di attacco più potente di un attacco con testo in chiaro noto. Ora l’attaccante
può scegliere testi in chiaro preparati appositamente, scelti per facilitare l’attacco di un
sistema. L’attaccante può scegliere un numero qualsiasi di testi in chiaro e ottenere i testi
cifrati corrispondenti. Di nuovo, non è un caso irrealistico; esistono parecchie situazioni
in cui un attaccante può scegliere i dati da cifrare. Spesso Alice otterrà delle informazioni
da qualche sorgente esterna (che potrebbe essere influenzata dall’attaccante) e quindi le
inoltrerà a Bob in forma cifrata. Per esempio, un attaccante potrebbe inviare a Alice una
email sapendo che Alice la inoltrerà a Bob.
Un buon algoritmo di cifratura non ha problemi a opporsi a un attacco con testo in
chiaro scelto, ma questo tipo di attacco non va affatto considerato impossibile. Siate
molto scettici se qualcuno dovesse tentare di convincervi che un attacco di questo tipo
non può riguardare il suo sistema.

30   Capitolo 2

Esistono due varianti di questo attacco. Nel caso dell’attacco offline, l’attaccante prepara
una lista di tutti i testi in chiaro che vuole far cifrare per ottenere i testi cifrati. Nell’attacco
online, l’attaccante sceglie nuovi testi in chiaro in base ai testi cifrati già ricevuti. Nella
maggior parte dei casi questa distinzione può essere ignorata. Normalmente si parlerà
della versione online dell’attacco, la più potente.

2.6.4 Il modello con testo cifrato scelto
Parlare di testo cifrato scelto in realtà è fuorviante. Questo tipo di attacco si dovrebbe
chiamare testo cifrato e testo in chiaro scelti, ma sarebbe un nome troppo lungo. In un
attacco di questo tipo l’attaccante può scegliere sia i testi in chiaro, sia i testi cifrati. Per
ogni testo in chiaro scelto, ottiene il testo cifrato corrispondente, e per ogni testo cifrato
scelto, ottiene il corrispondente testo in chiaro.
Ovviamente l’attacco con testo cifrato scelto è più potente di un attacco con testo in
chiaro scelto, dal momento che l’attaccante ha maggiore libertà. L’obiettivo è ancora il
recupero della chiave, con cui è possibile decifrare i testi cifrati. Di nuovo, qualsiasi schema
di crittografia ben fatto non ha problemi a contrastare un attacco di questo tipo.

2.6.5 L’obiettivo dell’attacco discriminante
Gli attacchi descritti in precedenza recuperano il testo in chiaro o la chiave di decifratura, mentre esistono altri attacchi che non recuperano la chiave, ma consentono di
decifrare un altro specifico messaggio. Ci sono anche attacchi che non recuperano un
messaggio, ma rivelano alcune informazioni parziali su di esso. Per esempio, dati dieci
testi in chiaro scelti e un undicesimo testo cifrato, potrebbe essere possibile capire se
il bit meno significativo dell’undicesimo testo cifrato è 1 o 0, anche se non è possibile
ricavare la chiave di decifratura corrispondente. Questo tipo di informazione può essere
molto preziosa per un attaccante.
Esistono troppe forme di attacco, e nuove forme vengono elaborate continuamente.
Quindi, da cosa ci dovremmo difendere?
È auspicabile difendersi da un attacco discriminante, ovvero da qualsiasi metodo non banale che rilevi una differenza tra lo schema di cifratura ideale e quello in uso. Questo
include tutti gli attacchi discussi finora, così come tutti gli attacchi ancora da scoprire.
Naturalmente occorre definire quale sia lo schema ideale, cosa che non abbiamo ancora
fatto. Nel prossimo capitolo cominceremo a chiarire meglio il tema.
Forse tutto ciò vi sembrerà piuttosto improbabile, ma in realtà non è così. La nostra esperienza mostra che è necessario che i componenti di base siano perfetti.Alcune funzioni di
crittografia hanno imperfezioni che le rendono vulnerabili agli attacchi discriminanti, ma
a parte questo sono perfettamente soddisfacenti. Ogni volta che si utilizzano tali funzioni,
ci si deve assicurare che queste imperfezioni non portino a qualche problema. In un
sistema con diversi componenti di base, si deve anche controllare se ogni combinazione
di imperfezioni possa causare problemi. Tutto ciò diventa presto insostenibile, e nella
pratica abbiamo trovato sistemi reali che soffrono di debolezze dovute a imperfezioni
conosciute nei loro componenti di base.

Introduzione alla crittografia    31

2.6.6 Altri tipi di attacchi
Finora abbiamo parlato principalmente di attacchi a funzioni di cifratura. Si possono
anche definire attacchi verso altre funzioni crittografiche, come l’autenticazione, le firme
digitali e così via. Discuteremo queste forme quando le incontreremo.
Anche per le funzioni di cifratura ci siamo limitati a discutere i modelli di attacco di base,
in cui un attaccante conosce o sceglie i testi in chiaro o i testi cifrati. A volte l’attaccante
sa anche quando i testi cifrati sono stati generati, oppure conosce la velocità delle operazioni di cifratura e decifratura. Le informazioni relative al tempo e alla lunghezza del
testo cifrato possono rivelare dati riservati sui testi crittografati. Gli attacchi che fanno uso
di queste informazioni aggiuntive sono chiamati information leakage (fuga di informazioni)
o side-channel (canale laterale).

2.7 Entriamo nei dettagli
Ora esaminiamo più in dettaglio due generiche tecniche di attacco.

2.7.1 Attacchi del compleanno
Gli attacchi del compleanno sono chiamati così dal cosiddetto paradosso del compleanno. Se
ci sono 23 persone in una stanza, la possibilità che due di esse compiano gli anni nello
stesso giorno è superiore al 50%. Questa è una probabilità sorprendentemente alta, dato
che ci sono 365 possibili giorni di compleanno.
L’attacco del compleanno si basa sul fatto che valori duplicati, chiamati anche collisioni,
si presentano molto più rapidamente di quanto ci si aspetterebbe. Supponete che un
sistema per transazioni finanziarie sicure utilizzi una nuova chiave di autenticazione a
64 bit per ogni transazione (per semplicità, supponiamo che non sia utilizzata alcuna
cifratura). Ci sono 264 (=18 · 1018, diciotto miliardi di miliardi) di possibili valori della
chiave, quindi dovrebbe essere piuttosto difficile da violare, giusto? Sbagliato! Dopo aver
visto circa 232 (=4 · 109, quattro miliardi) di transazioni, un attaccante può aspettarsi che
due transazioni abbiano la stessa chiave. Supponete che il primo messaggio autenticato
sia sempre lo stesso: “Sei pronto a ricevere una transazione?”. Se due transazioni hanno
la stessa chiave di autenticazione, allora anche i valori MAC nei loro primi messaggi
saranno gli stessi, cosa semplice da scoprire per un attaccante. Sapendo che le due chiavi
sono le stesse, un attaccante può ora inserire i messaggi della transazione più vecchia
nella nuova transazione, mentre questa è in corso. Dato che la chiave di autenticazione è
corretta, questi falsi messaggi vengono accettati, cosa che costituisce una chiara violazione
del sistema di transazioni finanziarie.
In generale, se un elemento può avere N diversi valori, ci si può aspettare la prima collisione dopo aver scelto circa N elementi casuali. Tralasciamo i dettagli esatti, ma N
è abbastanza vicino al valore reale. Per il paradosso del compleanno abbiamo N = 365 e
N ≈ 19. Il numero di persone necessarie prima che la probabilità di un giorno di compleanno duplicato superi il 50% è in effetti 23, ma N è abbastanza vicino per i nostri
scopi e in crittografia si utilizza spesso questa approssimazione. Un modo di vedere le
cose è il seguente: se si scelgono k elementi, allora ci sono k(k − 1)/2 coppie di elementi,
ognuna delle quali ha probabilità pari a 1/N di essere una coppia di valori uguali. Perciò
Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

32   Capitolo 2

la probabilità di trovare una collisione è vicina a k(k − 1)/2N. Quando k ≈ N, questa
probabilità è vicina al 50% (si tratta solo di approssimazioni, ma sono sufficientemente
buone per i nostri scopi).
Nella maggior parte dei casi parliamo di valori a n bit. Dato che ci sono 2n possibili valori,
servono quasi 2n = 2n/2 elementi nell’insieme prima di potersi attendere una collisione.
Parleremo spesso di questo limite 2n/2, chiamato limite del compleanno.

2.7.2 Attacchi meet-in-the-middle
Gli attacchi meet-in-the-middle sono parenti degli attacchi del compleanno (nel loro insieme li chiamiamo attacchi di collisione). Sono, però, più comuni e più efficaci. Invece di
attendere che una chiave si ripeta, l’attaccante può costruire una tabella di chiavi scelta
da lui stesso.
Torniamo al nostro esempio precedente sul sistema di transazioni finanziarie che utilizza una nuova chiave a 64 bit per autenticare ogni transazione. Utilizzando un attacco
meet-in-the-midde, l’attaccante può violare il sistema anche più a fondo. Ecco come:
sceglie 232 diverse chiavi casuali a 64 bit. Per ognuna di queste chiavi, calcola il MAC
per il messaggio:“Sei pronto per ricevere una transazione?” e memorizza sia il MAC che
la chiave in una tabella. Poi intercetta ogni transazione e verifica se il MAC del primo
messaggio appare nella tabella; se appare, allora c’è una buona probabilità che la chiave di
autenticazione per quella transazione sia la stessa chiave che l’attaccante ha utilizzato per
calcolare l’elemento della tabella, e quel valore della chiave è memorizzato proprio a fianco
del valore MAC nella tabella. Ora che l’attaccante conosce la chiave di autenticazione,
può inserire messaggi arbitrari di sua scelta nella transazione (l’attacco del compleanno
gli permetteva solo di inserire messaggi da una vecchia transazione).
Quante transazioni deve osservare l’attaccante? Ha precalcolato il MAC su una tra 232
possibili chiavi, quindi ogni volta che il sistema sceglie una chiave, c’è una probabilità su
232 di scegliere una di quelle che l’attaccante può riconoscere. Dopo circa 232 transazioni
l’attaccante si può aspettare una transazione che utilizza una chiave per cui ha già precalcolato un MAC. Il carico di lavoro totale per l’attaccante è pari a circa 232 passaggi di
precalcolo oltre all’osservazione di 232 transazioni, una quantità di lavoro molto inferiore
rispetto a quella necessaria per provare tutte le 264 possibili chiavi.
La differenza tra l’attacco del compleanno e l’attacco meet-in-the-middle è che in un
attacco del compleanno si aspetta che un singolo valore si ripeta due volte in uno stesso
insieme di elementi, mentre in un attacco meet-in-the-middle si hanno due insiemi e
si aspetta una sovrapposizione tra di essi. In entrambi i casi ci si può aspettare di trovare
il primo risultato dopo lo stesso numero di elementi.
Un attacco meet-in-the-middle è più flessibile di un attacco del compleanno. Esaminiamolo da un punto di vista più astratto. Supponiamo di avere N possibili valori. Il primo
insieme ha P elementi, il secondo ha Q elementi. Ci sono coppie PQ di elementi, e
ogni coppia ha una probabilità di corrispondenza pari a 1/N. Ci si aspetta una collisione
appena PQ/N è vicino a 1. La scelta più efficiente è P ≈ Q ≈ N. Questo è esattamente,
di nuovo, il limite del compleanno. L’attacco meet-in-the-middle fornisce una maggiore
flessibilità. A volte è più semplice ottenere elementi per uno degli insiemi piuttosto che
per l’altro. Il solo requisito è che PQ sia vicino a N. Si potrebbero scegliere P ≈ N1/3
e Q ≈ N2/33. Nell’esempio precedente, l’attaccante potrebbe preparare una lista di 240

Introduzione alla crittografia    33

possibili valori MAC per il primo messaggio e aspettarsi di trovare la prima chiave di
autenticazione dopo aver osservato solo 224 transazioni.
Quando analizziamo la facilità con cui si possa attaccare un sistema, utilizziamo spesso
una dimensione pari a N per entrambi gli insieme, perché questo generalmente minimizza il numero di passaggi che l’attaccante deve eseguire. Ciò richiede anche un’analisi
più dettagliata per verificare se gli elementi di un insieme possano essere più difficili da
ottenere rispetto a quelli di un altro insieme. Se voleste provare a eseguire realmente un
attacco meet-in-the-middle, dovreste scegliere accuratamente le dimensioni degli insiemi
per assicurarvi che PQ ≈ N al minor costo possibile.

2.8 Livelli di sicurezza
Lavorando a sufficienza, qualsiasi sistema crittografico reale può essere attaccato con
successo. La vera domanda è quanto lavoro sia necessario per violare un sistema. Un
modo semplice per quantificare il carico di lavoro necessario a un attaccante è quello di
confrontarlo con una ricerca esaustiva. Un attacco con ricerca esaustiva prova tutti i possibili
valori per un obiettivo, come la chiave. Se un attacco richiede 2235 passaggi, questo valore
corrisponde a una ricerca esaustiva per un valore a 235 bit.
Parliamo spesso di un attaccante che ha bisogno di un certo numero di passaggi, ma non
abbiamo ancora specificato che cosa sia un passaggio, in parte per pigrizia, ma anche
perché così si semplifica l’analisi. Quando si attacca un sistema di cifratura, calcolare
una singola cifratura di un dato messaggio con una data chiave può essere considerato
come un singolo passaggio. A volte il passaggio è la semplice lettura di un dato in una
tabella. Insomma, è un aspetto che può variare, ma in ogni caso un passaggio può essere
eseguito da un computer in un tempo molto breve. A volte può essere eseguito in un
ciclo di clock, a volte richiede un milione di cicli di clock, ma nei termini di carico di
lavoro che un attacco crittografico richiede, un singolo fattore di un milione non è poi
così importante. La facilità di usare un’analisi basata sui passaggi compensa ampiamente
le imprecisioni intrinseche. Si può sempre eseguire un’analisi più dettagliata per capire
quanto lavoro sia richiesto per un passaggio. Per una stima veloce, si assume sempre che
un singolo passaggio richieda un singolo ciclo di clock.
Qualsiasi sistema progettato oggi ha bisogno almeno di un livello di sicurezza a 128
bit. Questo significa che ogni attacco avrà bisogno di almeno 2128 passaggi. Un nuovo
sistema progettato oggi, se ha successo, ha una buona probabilità di essere operativo
ancora tra 30 anni, e dovrebbe fornire almeno 20 anni di riservatezza per i dati dopo il
momento in cui non sarà più utilizzato. Quindi si deve puntare a fornire sicurezza per
i prossimi 50 anni. È un obiettivo elevato, ma si è lavorato per estrapolare la legge di
Moore e applicarla alla crittografia. Un livello di sicurezza di 128 bit è sufficiente [85].
Qualcuno potrebbe anche sostenere che 100 bit siano sufficienti, o magari 110 bit, ma
le primitive crittografiche sono spesso progettate attorno alle potenze di due, quindi
utilizziamo 128 bit.
Questo concetto di livello di sicurezza è solo approssimato. Misuriamo solo il lavoro totale
che un attaccante deve svolgere, e ignoriamo aspetti come la memoria o le interazioni
con il sistema sul campo. Pensare solo al carico di lavoro di un attacco è già piuttosto
difficile; complicare il modello renderebbe l’analisi di sicurezza ancora più complessa
e aumenterebbe di molto la possibilità di tralasciare un punto vitale. Dato che il costo

34   Capitolo 2

di utilizzare un approccio semplice e conservativo è relativamente basso, utilizziamo il
semplice concetto di livello di sicurezza, che comunque è funzione dell’accesso a disposizione dell’avversario: è limitato al modello con testo in chiaro noto o può operare
seguendo il modello con testo in chiaro scelto? E quanti messaggi cifrati può vedere
nel suo attacco?

2.9 Prestazioni
La sicurezza non si ottiene gratis. Mentre gli esperti di crittografia cercano di rendere
gli algoritmi di crittografia più efficienti possibile, questi algoritmi sono a volte percepiti
come troppo lenti. Creare una crittografia ad hoc per ottenere maggiore efficienza può
essere molto rischioso. Nell’ambito della sicurezza, se si devia dal percorso segnato, si
dovrà eseguire un enorme lavoro di analisi per assicurarsi di non creare accidentalmente
un sistema debole. Tale analisi richiede crittografi esperti. Per la maggior parte dei sistemi, è molto più economico acquistare un computer più veloce piuttosto che andare
incontro a problemi e spese nel tentativo di progettare e implementare un sistema di
sicurezza più efficiente.
Inoltre, per la maggior parte dei sistemi le prestazioni della crittografia non rappresentano
un problema. Le CPU moderne sono talmente veloci da poter sostenere qualsiasi flusso
di dati che gestiscono. Per esempio, cifrare un collegamento a 100 Mb/s con l’algoritmo AES richiede solo il 20% dei cicli macchina di una CPU Pentium III a 1 GHz (in
realtà meno, dato che non si trasferiscono mai 100 Mb/s su tale collegamento, a causa
del carico aggiuntivo del protocollo di comunicazione).
Esistono tuttavia alcune situazioni in cui la crittografia crea un collo di bottiglia rilevante.
Un valido esempio sono i web server che utilizzano un gran numero di connessioni SSL.
L’inizializzazione di una connessione SSL usa la crittografia a chiave pubblica e richiede
una grande potenza di calcolo lato server. Invece di sviluppare un sostituto di SSL che
sia più efficiente per il server, è molto più economico e sicuro acquistare componenti
hardware in grado di gestire il protocollo SSL esistente in modo più rapido.
Recentemente abbiamo trovato una buona argomentazione per convincere le persone a
scegliere la sicurezza piuttosto che le prestazioni. “Ci sono già abbastanza sistemi veloci
e insicuri; non abbiamo bisogno di un altro”. Questo è decisamente vero. Le mezze
misure in sicurezza costano quasi quanto fare le cose al meglio, ma forniscono ben poca
sicurezza pratica. Crediamo fermamente che, se si implementa qualche aspetto che abbia
a che fare con la sicurezza, lo si debba fare bene.

2.10 Complessità
Più complesso è un sistema, più probabilmente avrà problemi di sicurezza. Ci piace dire
che la complessità è il peggior nemico della sicurezza. È un’affermazione semplice, ma
ci è servito un po’ di tempo per capirla veramente.
Una parte del problema è il processo di sviluppo per test e correzioni utilizzato fin troppo frequentemente: si compila qualcosa, si eseguono test per individuare errori, si torna
indietro a correggere gli errori, si cercano altri errori, e così via. Controlla, correggi,
ripeti. E si prosegue così finché le finanze della società o altri fattori impongono che il

Introduzione alla crittografia    35

prodotto sia consegnato. Di sicuro, il risultato funzionerà ragionevolmente bene, se viene
utilizzato solo per le funzioni per cui è stato verificato.
Questo metodo potrebbe essere sufficiente per quanto riguarda la funzionalità, ma è del
tutto inadeguato per i sistemi di sicurezza.
Il problema del metodo per test e correzioni è che il test mostra solo la presenza di errori,
e in realtà solo di quegli errori che chi esegue il test sta cercando. I sistemi di sicurezza
devono lavorare anche sotto l’attacco di persone capaci e male intenzionate. Il sistema
non può essere testato per tutte le situazioni a cui gli attaccanti potrebbero esporlo. La
fase di test può soltanto verificare la funzionalità; la sicurezza è assenza di funzionalità.
L’attaccante non dovrebbe essere in grado di ottenere una certa proprietà a prescindere
da che cosa faccia, e il test non può mostrare l’assenza di funzionalità. Il sistema deve
essere sicuro fin dall’inizio.
Considerate la seguente analogia. Supponete di scrivere un’applicazione di medie dimensioni in un linguaggio di programmazione diffuso. Gli errori vengono corretti fino
a ottenere una prima compilazione. Quindi, senza ulteriori verifiche, impacchettate il
programma e lo consegnate all’utente. Nessuno si aspetterebbe di ottenere un prodotto
funzionale procedendo in questo modo.
Tuttavia, questo è esattamente ciò che si fa normalmente per i sistemi di sicurezza. Tali
sistemi sono impossibili da verificare perché nessuno sa come testarli. Per definizione, un
attaccante ha successo quando trova un aspetto che non è stato testato. E se c’è qualche
bug, il prodotto è difettoso. Quindi, il solo modo per ottenere un sistema sicuro consiste
nel costruire un sistema molto robusto fin dalle fondamenta. E ciò richiede un sistema
semplice.
Il solo modo che conosciamo per costruire un sistema semplice è quello di costruirlo
in maniera modulare. Lo sappiamo tutti dallo sviluppo del software. Ma in questo caso
non possiamo permetterci nessun bug, quindi dobbiamo essere piuttosto spietati nella
modularizzazione. Questo porta a un’altra regola: la correttezza deve essere una proprietà locale. In altre parole, una parte del sistema dovrebbe comportarsi correttamente
indipendentemente dal fatto che il resto del sistema funzioni. No, non vogliamo sentire
affermazioni del tipo: “Questo non sarà un problema perché quest’altra parte del sistema non permetterà mai che accada”. L’altra parte potrebbe avere un bug, o potrebbe
cambiare in una versione futura. Ogni parte del sistema è responsabile del proprio funzionamento.

2.11 Esercizi
Esercizio 2.1 Considerate il principio di Kerckhoff. Fornite almeno due argomenti
a favore del principio di Kerckhoff e almeno due argomenti a sfavore. Quindi riportate
e argomentate il vostro punto di vista sulla validità del principio in questione.
Esercizio 2.2 Supponete che Alice e Bob stiano scambiandosi email via Internet.
Stanno inviando queste email dai loro computer portatili, che sono connessi a reti
wireless gratuite fornite dai loro pub preferiti. Elencate tutti i soggetti che potrebbero
essere in grado di attaccare il sistema e che cosa potrebbero essere in grado di ottenere.
Descrivete nella maniera più specifica possibile come Alice e Bob possano difendersi da
ciascun tipo di attacco identificato.

36   Capitolo 2

Esercizio 2.3 Considerate un gruppo di 30 persone che vogliono stabilire connessioni
a coppie sicure utilizzando la crittografia a chiave simmetrica. Quante chiavi devono
essere scambiate in totale?
Esercizio 2.4 Supponete che Bob riceva un messaggio firmato utilizzando uno
schema di firma digitale con la chiave di firma segreta di Alice. Questo prova che Alice
abbia visto il messaggio in questione e abbia scelto di firmarlo?
Esercizio 2.5 Supponete che le PKI, come descritte nel Paragrafo 2.5, non esistano.
Supponete che Alice ottenga una chiave pubblica P che sembrerebbe appartenere a
Bob. Come può Alice essere sicura che P appartenga realmente a Bob? Considerate la
domanda in ognuno dei seguenti scenari:
• Alice può parlare al telefono con Bob.
• Alice può parlare al telefono con qualcun altro di cui si fida (chiamiamolo Charlie)
e Charlie ha già verificato che P appartiene a Bob.
• Alice può comunicare con Charlie via email, e Charlie ha già verificato che P appartiene a Bob.
Esponete in modo esplicito ogni ipotesi aggiuntiva che occorre fare.
Esercizio 2.6 Supponete che un attacco di tipo con testo cifrato scelto non possa
recuperare la chiave di decifratura segreta per uno schema di cifratura. Questo significa
che lo schema di cifratura è sicuro?
Esercizio 2.7 Considerate un sistema crittografico a chiave simmetrica nel quale le
chiavi crittografiche sono selezionate in maniera causale da un insieme di tutte le stringhe
a n bit. Quanto dovrebbe essere grande n, approssimativamente, per fornire 128 bit di
sicurezza contro un attacco del compleanno?

Parte II

Sicurezza
del messaggio
In questa parte
• Capitolo 3 I cifrari a blocchi
• Capitolo 4 Le modalità dei cifrari a blocchi
• Capitolo 5 Le funzioni di hash
• Capitolo 6 I codici di autenticazione
dei messaggi
• Capitolo 7 Il canale sicuro
• Capitolo 8 Aspetti relativi
all’implementazione (I)

Capitolo 3

Cifrari a blocchi

I cifrari a blocchi rappresentano uno degli elementi
fondamentali dei sistemi di crittografia. Dispongono
di ampia letteratura e sono tra le parti meglio comprese della crittografia. I cifrari a blocchi rimangono
comunque dei componenti di base. Per la maggioranza delle applicazioni, probabilmente, non si utilizzerà mai un cifrario a blocchi in maniera diretta; al
contrario, il cifrario verrà utilizzato nella cosiddetta
“modalità operativa”, che affronteremo più avanti
nel libro. Lo scopo di questo capitolo è quello di
assicurare una più solida comprensione dei cifrari a
blocchi: che cosa sono, come sono considerati dagli
esperti di crittografia e come scegliere tra le diverse
possibilità.

3.1 Che cos’è un cifrario a blocchi?
Un cifrario a blocchi è una funzione di cifratura per
blocchi di dati di dimensioni fisse. La generazione
attuale di cifrari a blocchi prevede una dimensione
del blocco pari a 128 bit (16 byte). Questi cifrari
accettano testo in chiaro a 128 bit e generano testo
cifrato a 128 bit. Il cifrario a blocchi è reversibile;
esiste una funzione di decifratura che lavora sul testo
cifrato a 128 bit e lo decifra per ottenere il testo in
chiaro a 128 bit. Il testo in chiaro e il testo cifrato
hanno sempre la medesima dimensione e questa viene
definita dimensione del blocco del cifrario.
Per eseguire la cifratura utilizzando un cifrario a
blocchi è necessaria una chiave segreta. Senza di essa
non vi è modo di nascondere i messaggi. Come il
testo in chiaro e il testo cifrato, anche la chiave è una

Sommario
3.1 Che cos’è un cifrario a
blocchi?
3.2 Tipi di attacchi
3.3 Il cifrario a blocchi ideale
3.4 Definizione di sicurezza
dei cifrari a blocchi
3.5 Cifrari a blocchi reali
3.6 Esercizi

40   Capitolo 3

stringa di bit, spesso di lunghezza pari a 128 o 256 bit. Spesso si scrive E(K, p) o EK (p)
per indicare la cifratura del testo in chiaro p, con K a indicare la chiave, e D(K, c) o DK (c)
per indicare la decifratura del testo cifrato c con la chiave K.
I cifrari a blocchi sono utilizzati per molti scopi, in particolare per la cifratura delle informazioni. Per scopi di sicurezza, tuttavia, i cifrari a blocchi vengono utilizzati di rado
in maniera diretta. Si dovrebbe invece utilizzare una modalità di cifratura a blocchi, di cui
ci occuperemo nel Capitolo 4.
Nell’utilizzo dei cifrari a blocchi, come in qualsiasi attività di cifratura, seguiamo sempre il principio di Kerckhoff e partiamo dal presupposto che gli algoritmi di cifratura e
decifratura siano pubblicamente noti. Qualcuno trova difficile accettare questo fatto e
vuole cercare di mantenere segreti gli algoritmi. Non fidatevi mai di un cifrario a blocchi
segreto (o di qualsiasi altra primitiva crittografica segreta).
A volte è utile considerare un cifrario a blocchi come una tabella parametrizzata da
una chiave. Per ogni chiave fissa sarebbe possibile calcolare una tabella dati che mappa
l’intero testo in chiaro sul testo cifrato. Questa tabella sarebbe enorme. Per un cifrario a
blocchi con dimensione del blocco di 32 bit, la tabella raggiungerebbe 16 GB; per una
dimensione di 64 bit si arriverebbe a 150 milioni di TB; per una dimensione di 128 bit
si otterrebbero 5 · 1039 byte, un numero così grande da non trovare neppure un nome
adatto per definirlo. Ovviamente non è praticabile costruire una tabella di questo tipo
nella realtà, ma questo rimane un utile modello concettuale. Sappiamo anche che il
cifrario a blocchi è reversibile. In altre parole, non ci sono due voci uguali nella tabella,
altrimenti la funzione di decifratura non potrebbe tradurre il testo cifrato in un testo in
chiaro univoco. Questa grande tabella, perciò, conterrà ogni possibile valore del testo
cifrato esattamente una volta. In matematica questa si chiama permutazione: la tabella è
semplicemente un elenco di tutti gli elementi possibili in cui l’ordine è modificato. Un
cifrario a blocchi con una dimensione del blocco di k bit indica una permutazione su
valori di k bit per ciascuno dei valori di chiave.
Per fare un po’ di chiarezza, dato che spesso qui si rileva una certa confusione, un cifrario a blocchi non permuta i bit del testo in chiaro in input, ma prende tutti i 2k input
a k bit e fa corrispondere ciascuno a un unico output a k bit. Per esempio, se k = 8, un
input di 00000001 potrebbe essere cifrato in 0100000 con una determinata chiave, ma
potrebbe anche diventare 11011110 con una chiave diversa, in base al modo in cui è
stato progettato il cifrario a blocchi.

3.2 Tipi di attacchi
Data la definizione di cifrario a blocchi, la definizione di un cifrario a blocchi sicuro
sembra piuttosto semplice: è un cifrario a blocchi che mantiene segreto il testo in chiaro. Tuttavia, anche se questo è sicuramente uno dei requisiti, non è sufficiente. Questa
definizione richiede solamente che il cifrario a blocchi sia sicuro rispetto agli attacchi
con solo testo cifrato, nei quali l’attaccante riesce a conoscere solamente il testo cifrato
di un messaggio. Alcuni attacchi di questo tipo sono stati rilevati e pubblicati [74, 121],
ma sono stati condotti contro cifrari a blocchi ben conosciuti e definiti. La gran parte
degli attacchi pubblicati sono del tipo con testo in chiaro scelto (cfr. il Paragrafo 2.6
per una panoramica sui vari attacchi). Tutti questi tipi di attacchi si applicano ai cifrari a
blocchi, e ve ne sono altri che sono ancora più specifici per essi.

Cifrari a blocchi   41

Il primo è l’attacco a chiavi correlate. Introdotto per la prima volta da Biham nel 1993 [13],
un attacco a chiavi correlate presuppone che l’attaccante abbia accesso a svariate funzioni di cifratura. Tutte queste funzioni hanno una chiave sconosciuta, ma le loro chiavi
hanno una relazione nota all’attaccante. Sembra molto strano, ma questo tipo di attacco
si rivela utile verso sistemi reali [70]. Esistono infatti sistemi reali che utilizzano chiavi
diverse con una relazione conosciuta. Almeno un sistema proprietario cambia la chiave
per ogni messaggio incrementandola di 1. Messaggi consecutivi, pertanto, vengono cifrati
con chiavi numerate consecutivamente. Ne risulta che una relazione tra le chiavi come
questa può essere utilizzata per attaccare determinati cifrari a blocchi.
Esistono anche tipi di attacco più stravaganti. Quando abbiamo progettato il cifrario a
blocchi Twofish (Paragrafo 3.5.4), abbiamo introdotto il concetto di attacco a chiave scelta,
in cui l’attaccante specifica alcune parti della chiave e poi esegue un attacco a chiavi
correlate sulla parte rimanente [115] (la successiva analisi rivelò che questo attacco non
funziona con Twofish [50], ma potrebbe avere successo verso altri cifrari a blocchi).
Perché dovremmo prendere in considerazione tipi di attacco particolari come gli attacchi a chiavi correlate e gli attacchi a chiave scelta? Per svariati motivi. Innanzitutto
abbiamo visto sistemi reali nei quali un attacco a chiavi correlate sul cifrario a blocchi
era possibile, perciò questi attacchi non vanno affatto trascurati. Abbiamo visto anche
protocolli standardizzati che richiedevano implementazioni per codificare un cifrario a
blocchi con due chiavi correlate: una chiave K scelta in maniera casuale e una seconda
chiave K’ uguale a K più una costante fissa.
In secondo luogo, i cifrari a blocchi sono componenti di base molto utili, ma in quanto
componenti, vengono sottoposti ai più diversi utilizzi. Una tecnica standard per costruire una funzione di hash da un cifrario a blocchi è quella di Davies-Meyer [128]. In
una funzione di hash Davies-Meyer, l’attaccante arriva improvvisamente a scegliere la
chiave del cifrario a blocchi, e questo gli consente di portare attacchi a chiavi correlate
e a chiave scelta. Le funzioni di hash sono trattate nel Capitolo 5, ma in questo libro
non affronteremo nei dettagli la costruzione Davies-Meyer. Si può tranquillamente dire,
comunque, che qualsiasi definizione di sicurezza di un cifrario a blocchi che ignori questi
tipi di attacchi è incompleta.
Il cifrario a blocchi è un modulo che dovrebbe avere un’interfaccia semplice. L’interfaccia
più semplice è quella che garantisce la presenza di tutte le proprietà che chiunque si
aspetterebbe ragionevolmente che il cifrario abbia. Consentendo la presenza di imperfezioni nel cifrario a blocchi si aggiunge solo una grande dose di complessità, sotto forma
di dipendenze incrociate, a qualsiasi sistema che lo utilizzi. In breve, vogliamo strutturare
i cifrari a blocchi per la massima sicurezza. La sfida è quella di definire le proprietà che
ci si potrebbe ragionevolmente aspettare da un cifrario a blocchi.

3.3 Il cifrario a blocchi ideale
È davvero difficile definire che cosa sia un cifrario a blocchi. È qualcosa che si riconosce
quando lo si vede, ma non si riesce a definire. La comunità dei teorici ha cristallizzato
alcune di queste proprietà in definizioni specifiche, come pseudocasualità e super-pseudocasualità [6, 86, 94]. La stessa comunità dei cifrari a blocchi, invece, utilizza una definizione
molto più ampia, che prende in considerazione elementi come chiavi deboli e attacchi
con chiave scelta. L’approccio adottato qui punta a far comprendere che cos’è un cifrario

42   Capitolo 3

a blocchi secondo la comunità delle primitive dei cifrari. Questo è ciò che chiamiamo
un cifrario a blocchi “ideale”.
Che aspetto dovrebbe avere il cifrario a blocchi ideale? Dovrebbe essere una permutazione casuale. Dobbiamo essere più precisi: per ogni valore della chiave, si vuole che il
cifrario a blocchi sia una permutazione casuale, e le diverse permutazioni per i differenti
valori della chiave dovrebbero essere scelte in maniera indipendente. Come accennato nel
Paragrafo 3.1, si può pensare a un cifrario a blocchi a 128 bit (una singola permutazione
su valori a 128 bit) come a una enorme tabella dati di 2128 elementi di 128 bit ciascuno.
Il cifrario a blocchi ideale è formato da una di queste tabelle per ogni valore della chiave,
con ogni chiave scelta a caso dall’insieme di tutte le permutazioni possibili.
Per essere precisi, questa definizione del cifrario a blocchi ideale è incompleta, dato che
l’esatta scelta delle tabelle non è stata specificata. Quando si specificano le tabelle, però,
il cifrario ideale è fisso e non più casuale. Per formalizzare la definizione, non si può
parlare di un singolo cifrario a blocchi ideale, ma si deve trattare il cifrario a blocchi ideale
come una distribuzione di probabilità uniforme sull’insieme di tutti i possibili cifrari a
blocchi. Ogni volta che si utilizza il cifrario a blocchi ideale, si dovrà parlare in termini di
probabilità. Questa è una delizia per i matematici, ma la complessità aggiunta renderebbe
decisamente più complicate le nostre spiegazioni, quindi manterremo il nostro concetto
informale ma più semplice di cifrario a blocchi scelto in maniera casuale. Dobbiamo
anche sottolineare che un cifrario a blocchi ideale non è qualcosa che si possa ottenere
nella pratica; è un concetto astratto che si utilizza parlando di sicurezza.

3.4 Definizione di sicurezza dei cifrari a blocchi
Come si è notato in precedenza, esistono in letteratura definizioni formali di sicurezza
per i cifrari a blocchi. Per i nostri scopi possiamo utilizzare una definizione più semplice
ma informale.
Definizione 1
Un cifrario a blocchi sicuro è un cifrario contro il quale non esiste un attacco.

È una specie di tautologia. Ora infatti dobbiamo definire un attacco contro un cifrario
a blocchi.
Definizione 2
Un attacco contro un cifrario a blocchi è un metodo non generico in grado di distinguere
il cifrario in questione da un cifrario a blocchi ideale.

Che cosa intendiamo con “distinguere un cifrario a blocchi da un cifrario a blocchi
ideale”? Dato un cifrario a blocchi X, lo si paragona a un cifrario a blocchi ideale con
le stesse dimensioni di blocco e di chiave. Un discriminatore è un algoritmo a cui viene
fornita una funzione black-box che calcola o il cifrario a blocchi X o un cifrario a blocchi
ideale (una funzione black-box è una funzione che può essere valutata, ma di cui l’algoritmo discriminatore non conosce i meccanismi interni). Sono disponibili le funzioni
di cifratura e decifratura, e l’algoritmo discriminatore è libero di scegliere una qualsiasi
chiave per ciascuna delle cifrature e decifrature che svolge. Il compito del discriminatore

Cifrari a blocchi   43

è quello di capire se la funzione black-box sta implementando il cifrario a blocchi X o
il cifrario a blocchi ideale. Non occorre che sia un discriminatore perfetto, purché sia in
grado di fornire la risposta corretta per un numero di volte significativamente maggiore
rispetto alle risposte errate.
Esistono, ovviamente, soluzioni generiche (e banali) per tutto ciò. Si potrebbe cifrare
il testo in chiaro 0 con la chiave 0 e vedere se il risultato corrisponde a quanto ci si
aspettava di ottenere dal cifrario a blocchi X. Questo è un discriminatore, ma per trasformarlo in un attacco, è necessario che non sia generico. È qui che diventa difficile
definire la sicurezza dei cifrari a blocchi: non è possibile formalizzare la definizione di
“generico” e “non generico”. È un po’ come l’oscenità: si riconosce quando si vede (nel
1964 il giudice della corte suprema statunitense Potter Stewart usò queste parole per
definire l’oscenità: “Oggi non cercherò di definire ulteriormente il tipo di materiale...
lo so quando lo vedo”).
Un discriminatore è generico se è possibile trovare un discriminatore simile per quasi tutti
i cifrari a blocchi. Nel caso precedente, il discriminatore è generico perché è possibile
costruirne uno simile per qualsiasi cifrario a blocchi. Questo “attacco” consentirebbe
perfino di distinguere tra due cifrari a blocchi ideali. Naturalmente non c’è una ragione
pratica per voler distinguere tra due cifrari a blocchi ideali. Piuttosto, questo attacco è
generico perché potrebbe essere utilizzato per distinguere, se si volesse, tra due cifrari a
blocchi ideali. L’attacco non sfrutta alcuna proprietà interna del cifrario.
Si può anche creare un discriminatore generico più avanzato. Cifriamo il testo in chiaro 0
con tutte le chiavi nell’intervallo 1, ... , 232 e contiamo con quale frequenza si ha ciascun
valore per i primi 32 bit del testo cifrato. Supponiamo di rilevare che per un cifrario X il
valore t si ha per 5 volte invece dell’unica volta attesa. Questa è una proprietà che non è
attesa per il cifrario ideale e consentirebbe di distinguere X da questo. Si tratta ancora di
un discriminatore generico, dato che è possibile costruire facilmente qualcosa di simile
per ogni cifrario X (in effetti è estremamente improbabile che un cifrario non abbia un
valore adatto per t). Questo attacco è generico dal momento che, nel modo in cui viene
descritto, è applicabile a tutti i cifrari a blocchi e non sfrutta una debolezza specifica di
X. Un simile discriminatore permetterebbe perfino di distinguere tra due cifrari ideali.
Le cose si complicano se progettiamo un discriminatore in questo modo: prepariamo
un elenco di 1000 diverse statistiche che possiamo calcolare su un cifrario. Calcoliamo
ognuna di esse per il cifrario X e costruiamo il discriminatore in base alla statistica che
fornisce il risultato più significativo. Ci aspettiamo di trovare una statistica con un livello
di significatività di circa 1 su 1000. Naturalmente possiamo applicare la stessa tecnica
per individuare discriminatori per qualsiasi cifrario particolare, quindi si tratta di un
attacco generico, ma ora la natura generica non dipende soltanto dal discriminatore
stesso, ma anche dal modo in cui è stato trovato. Ecco perché nessuno è stato in grado
di formalizzare una definizione di attacchi generici e di sicurezza dei cifrari a blocchi.
Vorremmo davvero darvi una definizione chiara di sicurezza dei cifrari a blocchi, ma la
comunità crittografica non ha ancora conoscenze sufficienti sulla crittografia per essere
in grado di farlo in termini davvero generali. Invece, le definizioni formali esistenti spesso
limitano le capacità di un attaccante. Per esempio, potrebbero non permettere attacchi
con chiave scelta. Queste ipotesi potrebbero valere in alcuni casi, ma noi cerchiamo di
costruire cifrari a blocchi che siano molto più forti.
Non si deve dimenticare di limitare la quantità di calcolo consentita nel discriminatore.
Avremmo potuto farlo in maniera esplicita nella definizione, ma ciò avrebbe portato

44   Capitolo 3

un ulteriore livello di complicazione. Se il cifrario a blocchi ha un livello esplicito di
sicurezza di n bit, allora un discriminatore efficace dovrebbe essere più efficiente di una
ricerca completa su valori a n bit. Se non viene specificato esplicitamente un fattore di
forza del progetto, esso corrisponde alla lunghezza della chiave. Questa formulazione è
piuttosto tortuosa per un motivo: si sarebbe tentati di affermare che il discriminatore deve
lavorare in meno di 2n passi. Questo è senz’altro vero, ma alcuni tipi di discriminatore
forniscono solo un risultato probabilistico che è più simile a una ricerca a chiave parziale.
L’attacco potrebbe esibire un compromesso tra la quantità di lavoro e la probabilità di
distinguere il cifrario da quello ideale. Per esempio, una ricerca completa di metà dello
spazio delle chiavi richiede 2n−1 passaggi e fornisce la riposta corretta nel 75% delle volte
(se l’attaccante trova la chiave, conosce la risposta. Se non trova la chiave, ha ancora il
50% di possibilità di indovinare provando a caso. Pertanto, le possibilità totali di ottenere
la risposta esatta sono 0,5 + 0,5 · 0,5 = 0,75). Confrontando il discriminatore con simili
ricerche parziali sullo spazio delle chiavi, teniamo conto di questo naturale compromesso
e cessiamo classificare come attacchi le ricerche di questo tipo.
La nostra definizione di sicurezza del cifrario a blocchi copre tutte le possibili forme di
attacco. Gli attacchi con solo testo cifrato, con testo in chiaro noto, con testo in chiaro scelto, a chiavi correlate e tutti gli altri tipi implementano un discriminatore non
generico. Questo è il motivo per cui in questo libro utilizzeremo la nostra definizione
informale, che coglie anche l’essenza della paranoia professionale di cui abbiamo parlato
nel Capitolo 1: vogliamo intercettare qualsiasi cosa che potrebbe essere verosimilmente
considerata un attacco non generico.
E allora perché impiegare svariate pagine per definire che cosa sia un cifrario a blocchi
sicuro? Questa definizione è molto importante perché definisce un’interfaccia semplice
e pulita tra il cifrario a blocchi e il resto del sistema. Questo tipo di modularizzazione
è un marchio di garanzia di una buona progettazione. Nei sistemi di sicurezza, dove
la complessità è uno dei nemici principali, una buona modularizzazione è ancora più
importante che in altri campi. Una volta che un cifrario a blocchi soddisfa la nostra
definizione di sicurezza, può essere considerato come se fosse un cifrario ideale. Dopo
tutto, se non si comporta come un cifrario ideale nel sistema, significa che si è trovato
un discriminatore per il cifrario, il che significa che il cifrario non è sicuro in base alla
nostra definizione. Se si utilizza un cifrario a blocchi sicuro, non è più necessario ricordare eventuali particolarità o imperfezioni: il cifrario avrà tutte le proprietà attese da un
cifrario a blocchi. Questo facilita la progettazione di sistemi più grandi.
Ovviamente alcuni cifrari che non rispettano la nostra definizione nella pratica potrebbero rivelarsi “sufficientemente buoni” per una determinata applicazione così come
sono definiti, ma perché rischiare? Anche se i punti deboli di un particolare cifrario a
blocchi rispetto alla nostra definizione sono altamente teorici – per esempio per il fatto
di richiedere una quantità inverosimile di lavoro per essere sfruttati, e quindi di non essere troppo vulnerabili nella pratica – un cifrario a blocchi rispondente alla definizione
è molto più attraente.

3.4.1 Parità di una permutazione
Sfortunatamente c’è un’altra complicazione. Come si è visto nel Paragrafo 3.1, la cifratura con una chiave singola corrisponde a una ricerca in una tabella di permutazione.
Pensate a realizzare questa tabella in due fasi. Per prima cosa inizializzate la tabella con

Cifrari a blocchi   45

la mappatura di identità assegnando all’elemento di indice i il valore i. Quindi create la
permutazione desiderata scambiando ripetutamente due elementi nella tabella. Esistono
due tipi di permutazioni: quelle che possono essere costruite con un numero pari di
scambi (permutazioni pari) e quelle che possono essere ottenute con un numero dispari di
scambi (permutazioni dispari). Non dovreste essere sorpresi dal fatto che la metà di tutte
le permutazioni è pari e l’altra metà è dispari.
La maggior parte dei moderni cifrari a blocchi ha una dimensione del blocco di 128
bit, ma lavora su parole a 32 bit. Questi cifrari costruiscono la funzione di cifratura con
molte operazioni a 32 bit. Questo si è rivelato un metodo molto efficace, ma ha un effetto
collaterale. È piuttosto difficile costruire una permutazione dispari da poche operazioni;
di conseguenza, praticamente tutti i cifrari a blocchi generano solamente permutazioni
pari. Questo fornisce un semplice discriminatore per quasi qualsiasi cifrario a blocchi,
chiamato attacco di parità. Per una data chiave, si estrae la permutazione cifrando tutti
i possibili testi in chiaro. Se la permutazione è dispari, abbiamo un cifrario a blocchi
ideale, perché un cifrario a blocchi reale non genera mai una permutazione dispari;
se la permutazione è pari, abbiamo un cifrario a blocchi reale. Questo discriminatore
risponderà correttamente nel 75% dei casi; fornirà la risposta sbagliata solo se gli viene
passato un cifrario ideale che produce una permutazione pari. Il tasso di successo può
essere aumentato ripetendo il lavoro per altri valori di chiave.
Questo attacco non ha un significato pratico. Per trovare la parità di una permutazione,
si devono calcolare tutte le coppie testo in chiaro/testo cifrato della funzione di cifratura
tranne una (l’ultima può essere dedotta facilmente: l’unico testo in chiaro rimanente si
mappa sull’unico testo cifrato rimasto). In un sistema reale non si dovrebbe mai permettere di eseguire così tante richieste di testo in chiaro/testo cifrato a un cifrario a blocchi,
perché altri tipi di attacchi iniziano a fare male molto prima. In particolare, una volta che
l’attaccante conosce la maggior parte delle coppie testo in chiaro/testo cifrato, non ha
più bisogno di una chiave per decifrare il messaggio, ma può semplicemente utilizzare
una tabella dati creata a partire da queste coppie.
Potremmo dichiarare che l’attacco di parità è generico per definizione, ma sembrerebbe
ingannevole, dato che anche la parità dei cifrari a blocchi è un artefatto particolare della
loro progettazione. Piuttosto, preferiamo cambiare la definizione del cifrario a blocchi
ideale, limitandola a permutazioni pari scelte a caso.
Definizione 3
Un cifrario a blocchi ideale implementa una permutazione pari casuale scelta in maniera
indipendente per ciascuno dei valori della chiave.

È un peccato complicare il nostro cifrario “ideale” in questo modo, ma la sola alternativa
sarebbe quella di dequalificare qui tutti i cifrari a blocchi conosciuti. Per la stragrande
maggioranza delle applicazioni, la limitazione alle permutazioni pari è insignificante.
Purché non si consenta mai il calcolo delle coppie testo in chiaro/testo cifrato, è impossibile distinguere tra permutazioni pari e dispari.
Se vi trovate a che fare con un cifrario a blocchi in grado di generare permutazioni dispari, dovreste tornare alla definizione originale del cifrario ideale. In pratica, gli attacchi
di parità hanno maggiore effetto sulla definizione formale di sicurezza che sui sistemi
reali, pertanto potete anche dimenticarvi di tutta questa faccenda della parità.

46   Capitolo 3

Questa discussione serve anche come ulteriore esempio del modo di pensare degli
esperti di crittografia. È più importante esibire paranoia professionale e considerare un
sovrainsieme di attacchi, per poi scremare quelli non realistici, che partire dai soli attacchi
realistici e cercare di scoprirne di nuovi.

3.5 Cifrari a blocchi reali
Negli anni sono stati proposti centinaia di cifrari a blocchi. È molto semplice progettare
un nuovo cifrario a blocchi, mentre è terribilmente difficile progettarne uno di buona
qualità. Non stiamo parlando soltanto di sicurezza; il fatto che un cifrario a blocchi debba
essere sicuro è ovvio. Realizzare un cifrario a blocchi sicuro è una sfida in sé, ma diventa
ancora più difficile creare un cifrario che si riveli efficiente in una grande varietà di
applicazioni diverse (in precedenza abbiamo detto che avremmo preferito la sicurezza
alle prestazioni; tuttavia, dove possibile, conviene preferire entrambe).
Progettare cifrari a blocchi può essere divertente e istruttivo, ma non si deve utilizzare
un cifrario sconosciuto in un sistema reale. La comunità della crittografia non si fida di
un cifrario fino a quando questo non è stato esaminato in maniera approfondita da altri
esperti. Un prerequisito di base è che il cifrario sia stato pubblicato, ma ciò non basta.
Ci sono così tanti cifrari in circolazione che è difficile che vengano analizzati in maniera
efficace da esperti del settore. È molto meglio utilizzare uno dei cifrari ben conosciuti
e che sono già stati esaminati.
Praticamente tutti i cifrari a blocchi consistono di svariate ripetizioni di un cifrario a
blocchi debole, noto come ciclo. Molti di questi cicli deboli in sequenza formano un
cifrario a blocchi forte. Questa struttura è semplice da progettare e da implementare, ed
è anche di grande aiuto nell’analisi. La maggior parte degli attacchi ai cifrari a blocchi
inizia affrontando le versioni con un numero ridotto di cicli. A mano a mano che l’attacco migliora, diventa possibile attaccare più e più cicli.
Affronteremo in maggiore dettaglio molti cifrari a blocchi, ma non daremo una definizione completa. Le specifiche complete si possono trovare su Internet o nei manuali di
riferimento. Ci concentreremo invece sulla struttura globale e sulle proprietà di ciascun
cifrario.

3.5.1 DES
Il venerabile DES (Data Encryption Standard) [96] alla fine è sopravvissuto a se stesso. Le
ridotte dimensioni della chiave, 56 bit, e del blocco, 64 bit, lo rendono inutilizzabile data
la velocità dei computer oggi in circolazione e le grandi quantità di dati. Sopravvive
sotto forma di Triple DES [99], un cifrario a blocchi formato da tre cifrature DES in
sequenza – esegue una cifratura con DES con una chiave di 56 bit, una decifratura con
una seconda chiave di 56 bit e quindi torna a cifrare usando la prima chiave o una terza
chiave di 56 bit. Questo risolve il problema più impellente delle piccole dimensioni
della chiave, ma non c’è alcun rimedio per le dimensioni del blocco. Il DES non è un
cifrario particolarmente veloce per gli standard attuali, e il Triple DES funziona a un
terzo della velocità del DES. Anche se il DES si può trovare ancora in molti sistemi, non
raccomandiamo di utilizzare DES o Triple DES in nuovi progetti. Rimane comunque
un progetto classico, del tutto degno di essere studiato di per sé.

Cifrari a blocchi   47

La Figura 3.1 illustra il funzionamento di un ciclo DES. Si tratta di uno schema lineare
dei calcoli DES; schemi di questo tipo compaiono comunemente nella letteratura sulla
crittografia. Ogni casella calcola una particolare funzione, e le linee mostrano quale
valore sia usato e dove. Ci sono alcune convenzioni standard. L’operazione xor, o di or
esclusivo, chiamata anche addizione bit a bit o addizione senza raggruppamento, viene
mostrata nelle formule e nelle immagini con il simbolo ⊕. Si possono anche trovare
illustrazioni che comprendono addizioni intere che spesso vengono rappresentate con
un aspetto simile all’operatore .
L

R

Ki
F
Mescola bit

S

Espandi

Figura 3.1 Struttura di un singolo ciclo di DES.

Il DES ha un testo in chiaro di 64 bit che viene diviso in due metà di 32 bit, L e R.
Questa divisione viene effettuata risistemando i bit in una modalità semi ordinata. Nessuno sa perché i progettisti si siano presi la briga di ridisporre i bit del testo in chiaro (la
cosa non ha alcun effetto crittografico) ma il DES è definito così. Un simile scambio di
bit è implementato alla fine della cifratura per creare il testo cifrato a 64 bit dalle due
metà L e R.
Il DES è formato da 16 cicli numerati da 1 a 16. Ogni ciclo i usa una chiave di ciclo
separata a 48 bit Ki. Ogni chiave di ciclo viene formata scegliendo 48 bit dalla chiave a
56 bit, e questa scelta è differente per ciascuna chiave di ciclo (esiste struttura per questa
selezione, rintracciabile nelle specifiche del DES [96]). L’algoritmo che deriva queste chiavi di ciclo dal blocco principale della chiave del cifrario è chiamato gestore della chiave.
Il ciclo i trasforma la coppia (L, R) in una nuova coppia (L, R) sotto il controllo di una
chiave di ciclo Ki. La gran parte del lavoro è svolta dalla funzione di ciclo F, mostrata nella
casella tratteggiata. Come si vede nella figura, il valore R viene innanzitutto elaborato
da una funzione di espansione, che duplica un numero di bit per produrre un output di
48 bit dall’input di 32 bit. Sul risultato a 48 bit viene effettuato un or esclusivo con la
chiave di ciclo a 48 bit Ki. Il risultato dell’operazione viene utilizzato nelle tabelle S-box.
Una S-box (il termine deriva da substitution box, scatola di sostituzione) è in sostanza una
tabella di dati pubblicamente nota. Poiché non è possibile costruire una tabella di dati
con 48 bit di input, le S-box consistono di 8 piccole tabelle lookup, ciascuna delle quali
mappa 6 bit su 4 bit. Questo porta la dimensione risultante a 32 bit. Questi 32 bit quindi
vengono rimescolati con l’apposita funzione di scambio dei bit prima di subire un or
esclusivo che produce il valore di sinistra L. infine, i valori di L ed R vengono permutati.
Tutto questo calcolo viene ripetuto 16 volte per una singola cifratura DES.

48   Capitolo 3

La struttura di base del DES viene chiamata costrutto di Feistel [47]. Si tratta di un’idea
davvero elegante. Ciascun ciclo consiste in un or esclusivo di L con F(Ki, R) per qualche
funzione F, e quindi nella permuta di L ed R. La bellezza del costrutto sta nel fatto che
la decifratura richiede esattamente lo stesso insieme di operazioni delle cifratura. Si devono permutare L ed R, e poi fare un xor di L con F(Ki, R).Tutto ciò rende molto più
semplice implementare le funzioni di cifratura e decifratura insieme. Significa anche che
si rende necessario analizzare una sola delle due funzioni, dato che sono quasi identiche.
Un ultimo trucco utilizzato nella maggioranza dei cifrari Feistel consiste nel tralasciare
la permuta dopo l’ultimo ciclo, cosa che rende le funzioni di cifratura e decifratura
identiche tranne per l’ordine delle chiavi di ciclo. Questo è particolarmente utile nelle
implementazioni hardware, dato che diventa possibile utilizzare il medesimo circuito sia
per la cifratura che per la decifratura.
Le diverse parti del cifrario DES hanno funzioni diverse. La struttura di Feistel semplifica
la progettazione del cifrario e assicura che le due metà L ed R siano mescolate assieme.
L’or esclusivo sul materiale della chiave assicura che la chiave e i dati vengano mescolati,
e questo è il punto centrale di un cifrario. Le S-box forniscono la non linearità. Senza
di esse il cifrario potrebbe essere scritto come un gruppo di addizioni binarie, che consentirebbero un attacco matematico molto semplice basato sull’algebra lineare. Infine la
combinazione di S-box, espansione e funzioni di mescolamento dei bit consentono di
ottenere la “diffusione”, garantendo che, se un bit viene modificato nell’input di F, più
bit verranno modificati nell’output. Nel ciclo successivo ci saranno più cambi di bit,
e ancora di più nel ciclo seguente, e così via. Senza una buona diffusione, una piccola
modifica nel testo in chiaro porterebbe a una piccola modifica nel testo cifrato, che
sarebbe molto semplice da individuare.
DES ha numerose proprietà che lo dequalificano rispetto alla nostra definizione di
sicurezza. Ognuna delle chiavi di ciclo consiste solamente di alcuni dei bit selezionati
dalla chiave del cifrario. Se la chiave del cifrario è uguale a 0, allora tutte le chiavi di
ciclo saranno anch’esse uguali a 0. In particolare, tutte le chiavi di ciclo sono identiche.
Ricordate che l’unica differenza tra cifratura e decifratura sta nell’ordine delle chiavi di
ciclo. Ma qui tutte le chiavi di ciclo sono uguali a zero, perciò la cifratura con la chiave
0 coincide con la decifratura con la chiave 0. Questa è una proprietà che può essere
individuata molto facilmente, e poiché un cifrario a blocchi ideale non la possiede, porta
a un facile ed efficace attacco discriminante (ci sono altre tre chiavi che hanno questa
proprietà; insieme vengono chiamate le chiavi deboli del DES).
Il DES ha anche una proprietà di complementazione che assicura che:
E(K–, P–) = E(K, P)
per tutte le chiavi K e i testi in chiaro P, dove X è il valore ottenuto complementando tutti
i bit in X. In altre parole, se si esegue la cifratura del complemento del testo in chiaro con
il complemento della chiave, si ottiene il complemento del testo cifrato (originale).
È abbastanza facile capire. Osservate la Figura 3.1 e pensate a che cosa succede se si
alternano tutti i bit di L, R e Ki. La funzione di espansione copia semplicemente i bit,
per cui anche i bit dell’output saranno alternati. L’or esclusivo con la chiave Ki riceve
entrambi gli input alternati, per cui il risultato rimane identico. L’input alle S-box rimane
lo stesso, l’output in uscita dalle S-box rimane identico, per cui l’or esclusivo finale ha un
input alternato e uno identico. Perciò anche il nuovo valore di L che dovrà presto essere
scambiato alla posizione di R, viene scambiato. In altre parole, se si complementano L e

Cifrari a blocchi   49

R all’inizio del ciclo e si complementa anche Ki, allora l’output sarà il complemento di
quanto si aveva in origine. Questa proprietà attraversa tutto il cifrario. Il cifrario a blocchi
ideale non presenta questa curiosa proprietà e inoltre, cosa ancora più importante, questa
particolare proprietà può portare ad attacchi verso sistemi che utilizzano DES.
In breve, DES non supera più i requisiti. La proprietà illustrata in precedenza lo dequalifica rispetto alla nostra definizione di sicurezza. Ma anche non considerando le proprietà
descritte precedentemente, la lunghezza della chiave DES è totalmente inadeguata. Ci
sono già stati molti tentativi andati a buon fine di trovare una chiave DES utilizzando
una semplice ricerca esaustiva [44].
Triple DES ha una chiave più lunga, ma eredita dal DES sia le chiavi deboli che la complementazione, e ognuna di queste eredità è sufficiente per dequalificare il cifrario rispetto
ai nostri standard. Questo cifrario è anche notevolmente limitato dalla dimensione del
blocco di 64 bit, che impone grandi limiti sulla quantità di dati che può essere cifrata con
una singola chiave (maggiori dettagli sono riportati nel Paragrafo 4.8). A volte si rende
necessario ricorrere al Triple DES per motivi di supporto di vecchi sistemi, ma occorre
prestare grande attenzione nell’utilizzo, a causa delle piccole dimensioni del blocco e del
fatto che esso non si comporta come un cifrario a blocchi ideale.

3.5.2 AES
AES (Advanced Encryption Standard) è lo standard del governo statunitense creato per
sostituire DES. Invece di progettare o commissionare un cifrario, il NIST (National
Institute of Standards and Technology) chiese alla comunità della crittografia di avanzare
delle proposte. Furono presentate in totale 15 proposte [98]. Furono scelti come finalisti
cinque cifrari, [100], e infine Rijndael fu scelto come AES (si segnalano difficoltà nella
corretta pronuncia di “Rijndael”, ma non preoccupatevi, è difficile da pronunciare per
chi non parli l’olandese, quindi pronunciatelo come vi pare o chiamatelo semplicemente
“AES”). AES divenne uno standard nel 2001.
AES usa una struttura diversa da DES. Non si tratta di un cifrario Feistel. La Figura 3.2
mostra un singolo ciclo di AES. I cicli successivi sono simili. Il testo in chiaro arriva al
massimo a blocchi di 16 byte (128 bit). La prima operazione è un or esclusivo sul testo
in chiaro con 16 byte di chiave di ciclo. Questo passaggio è visualizzato con gli operatori ⊕; i byte della chiave entrano ai lati degli xor. Ognuno dei 16 byte viene quindi
utilizzato come indice in una tabella S-box che mappa input di 8 bit su output di 8 bit.
Le S-box sono tutte identiche. I byte quindi vengono riposizionati in un ordine specifico che sembra vagamente confuso, ma ha una struttura semplice. Infine, i byte sono
mescolati a gruppi di quattro mediante una funzione di mescolamento lineare. Il termine
“lineare” indica semplicemente che ogni bit di output della funzione di mescolamento
è l’or esclusivo di diversi bit di input.
Questo completa un singolo ciclo. Una cifratura completa è formata da 10 – 14 cicli,
in base alla lunghezza della chiave. AES è definito per chiavi a 128, 192 e 256 bit, e usa
10 cicli per chiavi a 128 bit, 12 cicli per chiavi a 192 bit e 14 cicli per chiavi a 256 bit.
Come per DES, è presente un gestore della chiave che genera le chiavi di ciclo necessarie,
ma la sua struttura è molto diversa.

50   Capitolo 3

S

S

S

Mix

S

S

S

S

S

Mix

S

S

S

Mix

S

S

S

S

S

Mix

Figura 3.2 Struttura di un singolo ciclo di AES.

La struttura di AES presenta vantaggi e svantaggi. Ogni passaggio è formato da un numero di operazioni che possono essere svolte in parallelo. Questo parallelismo facilita le
implementazioni ad alta velocità. D’altra parte, l’operazione di decifratura è molto diversa
da quella di cifratura. È necessaria la tabella dati inversa della S-box, e l’operazione di
mescolamento inversa è diversa dall’operazione di mescolamento originale.
Possiamo riconoscere alcuni degli stessi blocchi funzionali del DES. Gli xor aggiungono ai dati materiale della chiave, le S-box forniscono la non linearità, e le funzioni di
mescolamento dei byte forniscono la diffusione. AES ha uno schema molto pulito, con
compiti separati per ciascuna porzione del cifrario.
AES è sempre stato un cifrario progettato in maniera piuttosto aggressiva. Nella presentazione originale, i progettisti mostrarono un attacco su 6 cicli. Ciò significa che essi
erano a conoscenza della possibilità di un attacco se l’AES fosse stato progettato per
avere soltanto 6 cicli. Gli autori perciò scelsero da 10 a 14 cicli per l’intero cifrario, in
base alla lunghezza della chiave [27].
Durante il processo di selezione di AES, gli attacchi furono migliorati per gestire 7 cicli
per chiavi di 128 bit, 8 cicli per chiavi di 192 bit e 9 cicli per chiavi di 256 bit [49]. Così
rimaneva ancora un margine di sicurezza da 3 a 5 cicli. Da un’altra prospettiva, per le
chiavi a 128 bit il migliore attacco noto al momento della scelta di Rijndael come AES
copriva il 70% del cifrario. In altre parole, la scelta di Rijndael come AES si basava sul
presupposto che gli attacchi futuri non avrebbero esibito grandi miglioramenti.
AES supererà la prova nel tempo? Come sempre, è impossibile prevedere il futuro, ma a
volte giova dare uno sguardo al passato. Fino a poco tempo fa, i cifrari meglio analizzati
erano DES, FEAL e IDEA. In tutti i casi, gli attacchi erano migliorati significativamente
molti anni dopo la pubblicazione iniziale. Da allora il campo ha visto dei progressi, ma
servirebbe un atto di fede per pensare che si sappia già tutto e che non potranno essere
individuati grandi miglioramenti per gli attacchi. In effetti, già ora iniziano a vedersi
alcuni passi avanti stupefacenti nella crittoanalisi di AES [14, 15, 16]. Un attacco è in
grado di violare i 12 cicli di AES con chiavi a 192 bit utilizzando quattro chiavi correlate e 2176 passaggi, un altro è in grado di violare i 14 cicli di AES con chiavi di 256
bit usando 4 chiavi correlate e 2119 operazioni [15]. Un altro attacco può violare 10 dei

Cifrari a blocchi   51

14 cicli di AES con chiavi a 256 bit usando due chiavi correlate e solo 245 operazioni
[14]. Questi sono risultati impressionanti, che indicano che ora è noto che AES non
rispetta la nostra definizione di sicurezza per un cifrario a blocchi. Gli attacchi contro
le versioni complete a 192 e 256 bit sono teorici – non pratici – quindi non è ancora il
caso di perderci del sonno. Ma questi attacchi ricadono sotto la nostra definizione, per
cui teoricamente gli AES a 192 e 256 bit sono stati violati. E con l’andare del tempo
potrebbero esser scoperti attacchi ancora migliori.
La comunità sta ancora cercando di capire il significato di questi risultati per l’utilizzo
di AES in un sistema reale. Allo stato attuale delle conoscenze, l’impiego di AES sembra
ancora una decisione ragionevole. Rimane lo standard per il governo degli Stati Uniti, e
non è affatto poco. Usare lo standard permette di evitare un gran numero di discussioni
e problemi. Tuttavia, è importante comprendere che futuri miglioramenti della crittoanalisi potrebbero portare alla luce punti deboli ancora più gravi. Se state sviluppando
un sistema o standardizzando un protocollo, vi raccomandiamo di prevedere una certa
flessibilità o estensibilità nel caso in cui in futuro si renda necessario sostituire AES con
un diverso cifrario. Torneremo su questo argomento nel Paragrafo 3.5.6.

3.5.3 Serpent
Serpent era un altro dei finalisti per AES [1]. È costruito come un carroarmato. Era probabilmente la più prudente tra tutte le proposte per AES, e per molti aspetti è addirittura
l’opposto di AES. Laddove AES enfatizza eleganza ed efficienza, Serpent è progettato
solo per la sicurezza. Il migliore attacco conosciuto copre solo 12 dei 32 cicli [38]. Lo
svantaggio di Serpent sta nel fatto che ha una velocità pari a circa un terzo di quella di
AES. Può anche essere complicato da implementare in maniera efficiente, dato che le
S-box devono essere convertite in una formula booleana adatta per la CPU in uso.
Per certi aspetti, Serpent ha una struttura simile ad AES. È costituito da 32 cicli. Ogni
ciclo prevede un xor su una chiave di ciclo a 128 bit, l’applicazione di una funzione di
mescolamento lineare a 128 bit, e quindi l’applicazione di 32 S-box a 4 bit in parallelo.
In ogni ciclo le 32 S-box sono identiche, ma ci sono otto diverse S-box che vengono
usate ognuna a turno in un ciclo.
Serpent presenta un trucco di implementazione particolarmente interessante. Un’implementazione diretta sarebbe molto lenta, dato che ogni ciclo richiede 32 operazioni
di S-box e ci sono 32 cicli. In totale le operazioni di S-box sono 1024, ed eseguirle una
alla volta sarebbe molto lento. Il trucco è quello di riscrivere le S-box come formule
booleane. Ciascuno dei quattro bit di output viene scritto come formula booleana dei
quattro bit in input; poi la CPU calcola questa formula booleana direttamente, utilizzando
istruzioni and, or o xor. Il trucco è che una CPU a 32 bit è in grado di calcolare 32 Sbox in parallelo, dato che ogni posizione dei bit nei registri calcola la medesima funzione,
benché su dati di input diversi. Questo stile di implementazione viene chiamato bitslice.
Serpent è progettato espressamente per essere implementato in questo modo. La fase di
mescolamento è relativamente semplice da calcolare in un’implementazione bitslice.
Se Serpent fosse stato veloce come Rijndael (ora AES), sarebbe quasi certamente stato
scelto come AES, grazie alla sua progettazione. Ma la velocità è sempre un fattore relativo. Se si misura la velocità per byte cifrato, Serpent è quasi veloce come DES e molto
più veloce di Triple DES. Solo quando viene confrontato con gli altri finalisti per AES,
Serpent appare lento.

52   Capitolo 3

3.5.4 Twofish
Anche Twofish è stato uno dei finalisti per AES. Può essere visto come un compromesso
tra AES e Serpent. È quasi veloce come AES, ma ha un margine di sicurezza più ampio. Il
migliore attacco di cui si ha notizia opera su 8 dei 16 cicli. Il maggiore svantaggio di Twofish
è che la modifica della chiave di cifratura può rivelarsi piuttosto onerosa, dato che Twofish
viene implementato al meglio con una notevole quantità di precalcoli sulla chiave.
Twofish utilizza la stessa struttura Feistel di DES. La Figura 3.3 fornisce una panoramica
(c’è un motivo per cui questa figura è più grande e dettagliata delle altre: due degli autori
di questo libro facevano parte del gruppo di progettazione di Twofish, quindi ci è stato
possibile ricavare la figura direttamente dal nostro manuale su Twofish [115]). Twofish
suddivide il testo in chiaro a 128 bit in 4 valori a 32 bit, e la gran parte delle operazioni
viene svolta su valori a 32 bit. Potete vedere la struttura Feistel di Twofish, con F come
funzione di ciclo.

P (128 bit)
K0

K1
F

K2
g

K2r + 8

s0
s1
s2

K3

Mascheramento
input

<<<1

PHT
MDS

s3
g
<<<8

s0
s1
s2

un ciclo
MDS

s3

>>>1

K2r + 9

15 altri
cicli
annulla
ultimo
scambio
K4

K5

K6
C (128 bit)

Figura 3.3 Struttura di Twofish.
©1999, Niels Ferguson, John Wiley and Sons. Uso permesso.

K7

Mascheramento
output

Cifrari a blocchi   53

La funzione di ciclo consiste di due copie della funzione g, una funzione denominata
PHT e un’addizione di chiavi. Il risultato della funzione F viene sottoposto all’or esclusivo per ottenere la metà di destra (le due linee verticali sulla destra). Le caselle con i
simboli <<< e >>> indicano le rotazioni del valore a 32 bit per il numero specificato
di posizioni di bit.
Ogni funzione g consiste di quattro S-box seguite da una funzione di mescolamento
lineare molto simile a quella di AES. Le S-box sono in certa misura diverse. A differenza
di quanto avviene per tutti gli altri cifrari a blocchi incontrati in questo libro, queste Sbox non sono costanti; anzi, i loro contenuti dipendono dalla chiave. C’è un algoritmo
che calcola le tabelle S-box dal materiale di chiave. Il motivo di questo tipo di progettazione sta nel fatto che S-box legate alla chiave risultano più difficili da analizzare per
un attaccante. Questo è anche il motivo per cui le implementazioni di Twofish spesso
svolgono attività di precalcolo per ciascuna chiave. Vengono precalcolate le S-box e il
risultato viene salvato in memoria.
La funzione PHT mescola i due risultati delle funzioni g usando operazioni di addizione
a 32 bit. L’ultima parte della funzione F è il punto in cui viene inserito il materiale di
chiave. Notate come l’addizione sia visualizzata con e l’or esclusivo con ⊕.
Twofish utilizza anche il whitening (mascheramento). Sia all’inizio che alla fine del cifrario,
ulteriore materiale di chiave viene aggiunto ai dati. Questo rende il cifrario più difficile
da attaccare per la maggior parte dei tipi di attacchi, e a un costo davvero basso.
Come gli altri cifrari,Twofish ha un gestore della chiave per derivare le chiavi di ciclo e
le due chiavi aggiuntive all’inizio e alla fine dall’effettiva chiave di cifratura.

3.5.5 Altri finalisti per AES
Abbiamo esaminato in un certo dettaglio tre dei cinque finalisti per AES. Ce n’erano
altri due: RC6 [108] e MARS [22].
RC6 è un progetto interessante che utilizza nel cifrario moltiplicazioni a 32 bit. Durante
il concorso per AES, l’attacco migliore riuscì a violare una versione di RC6 a 17 cicli,
rispetto ai 20 cicli della versione completa. MARS è un progetto con una struttura non
uniforme. Utilizza un gran numero di operazioni diverse e risulta pertanto più costoso
da implementare rispetto agli altri finalisti.
Sia RC6 che MARS furono scelti come finalisti per AES per un motivo: entrambi sono
probabilmente buoni cifrari a blocchi. I dettagli sul loro funzionamento interno sono
riportati nelle rispettive specifiche.

3.5.6 Quale cifrario a blocchi scegliere?
I recenti progressi della crittoanalisi rispetto ad AES rendono la scelta difficile. Nonostante questi progressi, AES rimane il cifrario che ci sentiamo di raccomandare. È veloce;
tutti gli attacchi conosciuti sono teorici, non pratici. Anche se oggi AES è stato violato
a livello accademico, questo non implica nella pratica un significativo scadimento della
sicurezza dei sistemi reali. AES inoltre costituisce lo standard ufficiale, autorizzato dal
governo degli Stati Uniti. Ed è utilizzato da tutti. Una volta si diceva: “Nessuno verrà
licenziato per avere comprato IBM”. Nessuno vi licenzierà per avere scelto AES.

54   Capitolo 3

AES presenta anche altri vantaggi: è relativamente facile da utilizzare e implementare,
tutte le librerie di crittografia lo supportano, e piace ai clienti perché è considerato lo
standard di riferimento.
Probabilmente ci sono circostanze in cui Triple DES rappresenta ancora la soluzione
migliore. Se dovete mantenere la compatibilità con vecchie versioni dei sistemi, o siete
costretti a usare una lunghezza del blocco di 64 bit per rispettare i requisiti di altre
parti del sistema, allora Triple DES è ancora la scelta migliore. Ricordate, comunque,
che Triple DES ha alcune proprietà uniche che gli impediscono di soddisfare i nostri
criteri di sicurezza; e prestate attenzione soprattutto alla piccola dimensione del blocco,
di soli 64 bit.
Se siete veramente preoccupati dei progressi futuri della crittoanalisi, potete ricorrere a
una doppia cifratura, prima con AES e quindi con Serpent o con Twofish. Se decidete
in questo senso, ricordate di utilizzare chiavi diverse e indipendenti per ciascun cifrario.
Oppure usate AES con un numero maggiore di cicli, per esempio diciamo 16 cicli per
AES con chiavi a 128 bit, 20 cicli con chiavi a 192 bit e 28 cicli con chiavi a 256 bit.
In più, ricordate che i progressi della crittoanalisi contro AES stanno iniziando a diffondersi proprio mentre stiamo scrivendo questo libro. È troppo presto per dire con
esattezza quali saranno le risposte della comunità. Tenete d’occhio il consenso generale
o cambiate direzione rispetto alla comunità. Forse il NIST emanerà qualche raccomandazione specifica su come affrontare le recenti scoperte contro AES. In questo caso, o se
vi sarà un mutamento di consenso nella comunità, nessuno vi farà pesare il fatto di voler
seguire quelle raccomandazioni o quella direzione.
Dobbiamo ritornare su un altro problema di AES. Non abbiamo parlato molto degli
attacchi a canale laterale (side channel) o degli attacchi di temporizzazione (ne parleremo
nei Paragrafi 8.5 e 15.3). Anche se non esistono attacchi pratici conosciuti contro le
componenti matematiche di AES, è possibile che AES sia implementato in modo scadente. Per esempio, qualcuno potrebbe implementare AES in modo tale che il tempo
necessario per svolgere un’operazione dipenda dai suoi input: per alcuni input occorrerà
più tempo, per altri meno. Se un attaccante è in grado di misurare il tempo impiegato
da un sistema per svolgere un’operazione AES, occorre prestare attenzione a utilizzare
un’implementazione a tempo costante, oppure è necessario nascondere in altro modo
all’attaccante le informazioni sulla temporizzazione.

3.5.7 Che lunghezza della chiave usare?
Tutti i finalisti per AES (Rijndael, Serpent,Twofish, RC6 e MARS), e pertanto lo stesso
AES, supportano chiavi di 128, 192 e 256 bit. Per quasi tutte le applicazioni, un livello
di sicurezza a 128 bit è sufficiente.Tuttavia, per ottenere 128 bit di sicurezza, suggeriamo
di utilizzare chiavi più lunghe di 128 bit.
Una chiave di 128 bit sarebbe ottima, tranne per un problema: gli attacchi di collisione.
Di tanto in tanto troviamo sistemi che possono essere attaccati – almeno in teoria se
non in pratica – da un attacco del compleanno o da un attacco meet-in-the-middle.
Sappiamo dell’esistenza di questi attacchi. A volte i progettisti si limitano a ignorarli, e a
volte pensano di essere al sicuro, ma qualcuno trova un modo nuovo e brillante di usarli.
La maggior parte dei cifrari a blocchi consente qualche tipo di attacco meet-in-themiddle. Ora ecco il nostro consiglio: per un livello di sicurezza di n bit, ciascun valore
crittografico dovrebbe avere lunghezza uguale almeno a 2n bit.

Cifrari a blocchi   55

Seguendo questa raccomandazione si rende inutile qualsiasi tipo di attacco di collisione.
Nella vita reale è difficile attenersi rigidamente a questa regola. Per una sicurezza a 128
bit, vorremmo davvero utilizzare un cifrario con una dimensione del blocco di 256 bit,
ma tutti i normali cifrari hanno una dimensione del blocco di 128 bit. E questo è un
problema più serio di quanto possa sembrare. Esistono infatti molti attacchi di collisione
a modalità di cifratura a blocchi, dei quali di occuperemo in seguito.
Possiamo almeno utilizzare le lunghe chiavi supportate da tutti i cifrari a blocchi candidati
per AES. Pertanto, consigliamo di usare chiavi di 256 bit. Non stiamo dicendo che le
chiavi di 128 bit siano insicure di per sé, ma le chiavi di 256 bit forniscono un migliore
margine di sicurezza, partendo dal presupposto che il cifrario a blocchi sia sicuro.
Notate che sosteniamo l’impiego di chiavi a 256 bit per sistemi con una forza di progetto di 128 bit. In altre parole, questi sistemi sono progettati per resistere ad attacchi che
prevedono 2128 passaggi. Ricordate solo, per dimensionare la parte rimanente del sistema,
di utilizzare la forza di progetto (128 bit) e non la lunghezza della chiave di 256 bit.
Torniamo infine ai recenti risultati della crittoanalisi contro AES. Questi risultati mostrano
come i cifrari AES con chiavi a 192 e 256 bit non siano sicuri. Inoltre, gli attacchi contro
AES con chiavi di 192 e 256 bit sfruttano debolezze presenti nell’algoritmo del gestore
della chiave. Questo è il motivo per cui gli attacchi conosciuti contro AES con chiavi
di 256 bit sono più efficienti degli attacchi contro AES con chiavi a 192 bit. Questo è
anche il motivo per il quale non vi è ancora notizia di attacchi contro AES con chiavi a
128 bit. Quindi, mentre in generale preferiremmo un cifrario a blocchi con chiavi a 256
bit rispetto a un cifrario con chiavi a 128 bit, partendo dal presupposto che il cifrario
sia sicuro, la situazione per AES è leggermente meno chiara. Per sottolineare il nostro
desiderio di 128 bit di sicurezza, e di conseguenza la nostra ricerca di un cifrario a blocchi
sicuro con chiavi a 256 bit, utilizzeremo AES con chiavi a 256 bit per il resto del libro.
Ma una volta ottenuto un chiaro consenso su come rispondere ai nuovi risultati della
crittoanalisi contro AES, molto probabilmente sostituiremo AES con un altro cifrario a
blocchi con chiavi a 256 bit.

3.6 Esercizi
Esercizio 3.1 Quanto spazio occorrerebbe per memorizzare una tabella per un intero
cifrario a blocchi idealizzato che lavori su blocchi di 64 bit e con chiavi a 80 bit?
Esercizio 3.2 Quanti cicli ci sono in DES? Quanti bit ci sono in una chiave DES?
Quali sono le dimensioni del blocco di DES? Come lavora Triple DES in funzione di
DES?
Esercizio 3.3 Quali sono le lunghezze possibili per le chiavi di AES? Per ciascuna
chiave quanti cicli sono presenti in AES? Che dimensioni del blocco presenta AES?
Esercizio 3.4 In quali situazioni scegliereste Triple DES anziché AES? In quali situazioni scegliereste AES anziché Triple DES?

56   Capitolo 3

Esercizio 3.5 Supponete di avere un processore in grado di svolgere una singola
cifratura o decifratura DES in 2−26 secondi. Supponete anche di avere un gran numero
di coppie testo in chiaro/testo cifrato per DES sotto una sola chiave sconosciuta. Quante
ore occorrerebbero, in media, per trovare la chiave usando un approccio a ricerca esaustiva
e un singolo processore? Quante ore occorrerebbero, in media, per individuare la chiave
usando un approccio a ricerca esaustiva e un gruppo di 214 processori?
Esercizio 3.6 Pensate a un nuovo cifrario a blocchi, DES2, formato solamente da
due cicli del cifrario a blocchi DES. Il cifrario DES2 ha la stessa dimensione del blocco
e la stessa lunghezza della chiave di DES. Per questo esercizio dovreste considerare la
funzione F di DES come una black-box che richiede due input, un segmento di dati di
32 bit e una chiave di ciclo di 48 bit e produce un output di 32 bit.
Supponete di avere un gran numero di coppie testo in chiaro/testo cifrato per DES2 sotto
una singola chiave sconosciuta. Fornite un algoritmo per ottenere la chiave di ciclo di 48
bit per il ciclo 1 e la chiave di ciclo di 48 bit per il ciclo 2. Il vostro algoritmo dovrebbe
richiedere meno operazioni di una ricerca esaustiva per un’intera chiave DES di 56 bit.
Il vostro algoritmo può essere convertito in un attacco discriminante contro DES2?
Esercizio 3.7 Descrivete un sistema di esempio che utilizza DES ma è insicuro a
causa della proprietà di complementazione di DES. Nello specifico, descrivete il sistema
e poi presentate un attacco contro di esso; l’attacco dovrebbe utilizzare la proprietà di
complementazione di DES.
Esercizio 3.8 Prendete confidenza con un pacchetto di sviluppo di software crittografico per il vostro computer. OpenSSL è un pacchetto open source molto noto, ma
esistono anche diverse alternative.
Servendovi di una libreria crittografica esistente, decifrate il seguente testo cifrato (in
esadecimale);
53 9B 33 3B 39 70 6D 14 90 28 CF E1 D9 D4 A4 07

con la seguente chiave a 256 bit (anch’essa in esadecimale) utilizzando AES:
80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01

Esercizio 3.9 Usando una libreria crittografica esistente, cifrate il seguente testo in
chiaro (in esadecimale):
29 6C 93 FD F4 99 AA EB 41 94 BA BC 2E 63 56 1D

con la seguente chiave a 256 bit (anch’essa in esadecimale) usando AES.
80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01

Esercizio 3.10 Scrivete un programma che dimostra in via sperimentale la proprietà
di complementazione per il DES. Questo programma dovrebbe accettare come input
una chiave K e un testo in chiaro P e mostrare che la proprietà di complementazione
è valida per questa chiave e questo testo in chiaro. Per questo esercizio potete utilizzare
una libreria crittografica esistente.

Capitolo 4

Le modalità
dei cifrari a blocchi
I cifrari a blocchi cifrano solamente blocchi di dimensioni fisse. Se si vuole cifrare qualcosa che non
abbia le esatte dimensioni di un blocco, è necessario
utilizzare una modalità dei cifrari a blocchi; si tratta in
sostanza di funzioni di cifratura costruite mediante
un cifrario a blocchi.
Prima di proseguire con questo capitolo, un avvertimento: le modalità di cifratura descritte qui impediscono a un malintenzionato in ascolto di intercettare
il traffico, ma non forniscono alcuna autenticazione,
per cui un attaccante potrebbe ancora modificare il
messaggio, anche a piacimento, talvolta. Molti trovano
questo fatto sorprendente, ma è semplice da capire. La
funzione di decifratura di una modalità di cifratura si
limita a decifrare i dati. Potrebbe anche generare un
risultato senza senso, ma comunque decifra un testo
cifrato (modificato) in un testo in chiaro (modificato
e con probabilità di essere privo di significato). Non
si dovrebbe mai fare affidamento sul fatto che dei
messaggi senza senso non siano in grado di produrre
danni. Ciò comporta il fidarsi di altre parti del sistema, il che troppo spesso è causa di gravi dispiaceri.
Inoltre, per alcuni schemi di cifratura, la decifratura
dei testi cifrati modificati potrebbe non produrre
spazzatura; alcune modalità consentono la modifica
dei testi in chiaro oggetto di attacco e molti formati
di dati possono essere manipolati anche effettuando
localmente modifiche casuali.
In quasi tutte le situazioni, il danno potenzialmente prodotto dai messaggi modificati può essere molto più grave
di quello causato dalla divulgazione del testo in chiaro.
Pertanto, si dovrebbe sempre combinare la cifratura con
l’autenticazione, come vedremo nel Capitolo 6.

Sommario
4.1 Riempimento
4.2 ECB
4.3 CBC
4.4 OFB
4.5 CTR
4.6 Cifratura e
autenticazione insieme
4.7 Quale modalità
utilizzare?
4.8 Fuga di informazioni
4.9 Esercizi

58   Capitolo 4

4.1 Riempimento
In generale, una modalità di cifrario a blocchi è un modo per cifrare un testo in chiaro
P ottenendo un testo cifrato C, dove testo in chiaro e testo cifrato hanno lunghezza
arbitraria. La maggior parte delle modalità richiede che la lunghezza del testo in chiaro
sia un multiplo esatto della dimensione del blocco. Questo implica la necessità di un
riempimento (padding in inglese). Esistono molti modi per allungare il testo in chiaro,
ma la regola più importante prevede che il riempimento debba essere reversibile: deve
essere possibile risalire in maniera univoca al messaggio originale partendo da un messaggio allungato.
Spesso si incontra una regola di riempimento molto semplice che consiste nell’aggiunta
di zeri fino al raggiungimento della lunghezza adatta. Questa non è una buona idea.
Non è reversibile, dato che il testo in chiaro p e p || 0 coincidono dopo il riempimento
(utilizziamo l’operatore || per indicare la concatenazione).
In tutto il libro esamineremo solamente testi in chiaro di lunghezza pari a un numero
intero di byte. Alcune primitive crittografiche sono specificate per dimensioni dispari
dove l’ultimo byte non viene usato interamente. Non abbiamo mai trovato utile tale
generalizzazione, e spesso rappresenta un ostacolo. Molte implementazioni non prevedono
queste dimensioni dispari, quindi ci limiteremo a considerare dimensioni in byte.
Sarebbe bello avere una regola di riempimento che non allunghi il testo in chiaro se le
sue dimensioni sono già accettabili, ma non può ottenere ciò in tutte le situazioni. È
possibile mostrare che almeno alcuni messaggi che hanno già una lunghezza adatta devono
essere allungati con un qualsiasi schema di riempimento, e nella pratica tutte le regole di
riempimento aggiungono un minimo di un byte alla lunghezza del testo in chiaro.
E allora, come si riempie un testo in chiaro? Sia P il testo in chiaro e (P) la lunghezza
in byte di P. Sia b la dimensione in byte del blocco del cifrario a blocchi. Suggeriamo di
usare uno dei due semplici schemi di riempimento descritti di seguito.
1. Aggiungere un singolo byte con il valore 128 e poi tutti i byte a zero necessari per
portare la lunghezza totale a un multiplo di b. Il numero dei byte a zero aggiunti è
compreso nell’intervallo 0, ... , b − 1.
2. Determinare il numero dei byte di riempimento necessari. Questo è un numero n
che soddisfa la condizione 1 ≤ n ≤ b e n + (P) è un multiplo di b. Si allunga il testo
in chiaro aggiungendo n byte, ciascuno di valore n.
Entrambi questi schemi di riempimento funzionano bene, senza conseguenze sulla
crittografia. Qualsiasi schema di riempimento è accettabile, purché si reversibile. I due
proposti precedentemente sono solamente i più semplici. Si potrebbe anche inserire la
lunghezza di P all’inizio, poi P e poi riempire fino a un certo limite del blocco. Questo
però implicherebbe che la lunghezza di P sia nota quando inizia il trattamento dei dati,
e ciò potrebbe non essere vero.
Una volta che la lunghezza dopo il riempimento raggiunge un multiplo delle dimensioni
del blocco, il testo in chiaro riempito viene suddiviso in blocchi. Il testo in chiaro P
viene così trasformato in una sequenza di blocchi P1, ... , Pk. Il numero dei blocchi k può
essere calcolato come l(P) + 1/b, dove   indica la funzione di parte intera superiore
(o ceiling) che arrotonda un numero verso l’alto all’intero successivo. Nella parte restante
di questo capitolo assumeremo semplicemente che il testo in chiaro P sia formato da un
numero intero di blocchi P1, ... , Pk.

Le modalità dei cifrari a blocchi    59

Dopo aver decifrato il testo cifrato usando una delle modalità di cifrario a blocchi che
esamineremo, il riempimento deve essere rimosso. Il codice incaricato di questo compito
dovrebbe anche controllare che il riempimento sia stato inserito nella maniera corretta.
Ognuno dei byte di riempimento deve essere verificato per assicurare che abbia il valore esatto. Un riempimento anomalo dovrebbe essere trattato allo stesso modo di una
mancata autenticazione.

4.2 ECB
La modalità più semplice per cifrare un testo in chiaro più lungo di un blocco è nota
come modalità ECB (Electronic CodeBook), ed è definita come segue:
Ci = E(K, Pi)

per i = 1, ... , k

È piuttosto semplice: si procede cifrando separatamente ciascun blocco del messaggio.
Naturalmente le cose non possono essere così semplici, altrimenti non avremmo dedicato
un intero capitolo alla discussione delle modalità dei cifrari a blocchi. Non usate mai la
modalità ECB; ha dei difetti seri, e la citiamo soltanto per avvisarvi di starne lontani.
Qual è il problema della modalità ECB? Se due blocchi di testo in chiaro sono uguali,
allora i corrispondenti blocchi di testo cifrato saranno identici, è questo è visibile a un
attaccante. In base alla struttura del messaggio, questo fatto potrebbe rivelare molte informazioni a chi attacca.
Esistono molte situazioni in cui grossi blocchi di testo vengono ripetuti. Per esempio,
questo capitolo contiene molte occorrenze delle parole “blocco di testo cifrato”. Se due
di queste occorrenze si trovassero allineate alla fine di un blocco, si avrebbe la ripetizione
di un valore di blocco del testo in chiaro. Nella maggior parte delle stringhe Unicode, un
byte ogni due è uno zero, e questo aumenta notevolmente la possibilità della ripetizione
di un valore di blocco. Molti formati di file avranno blocchi di grandi dimensioni costituiti solo da zeri, e questo porta alla ripetizione di valori di blocco. In generale, questa
proprietà della modalità ECB la rende troppo debole per poter essere utilizzata.

4.3 CBC
La modalità CBC (Cipher Block Chaining) è una delle più utilizzate. I problemi dell’ECB
vengono evitati eseguendo l’or esclusivo di ogni blocco di testo in chiaro con il blocco di
testo cifrato precedente. La formulazione standard della modalità CBC è la seguente:
Ci = E(K, Pi ⊕ Ci–1)

per i = 1, ... , k

I problemi della modalità ECB vengono evitati “casualizzando” il testo in chiaro mediante il blocco di testo cifrato precedente. Blocchi uguali di testo in chiaro produrranno
tipicamente blocchi differenti di testo cifrato, e così si riducono in maniera significativa
le informazioni disponibili per l’attaccante.
Ci rimane ancora la domanda di quale valore usare per C0. Questo valore viene chiamato vettore di inizializzazione, o IV (dall’inglese Initialization Vector).Vedremo di seguito
le strategie per la scelta dell’IV.

60   Capitolo 4

4.3.1 IV fisso
Non si dovrebbe utilizzare un IV fisso, che introduce il problema della modalità ECB per
il primo blocco di ogni messaggio. Se due messaggi diversi iniziano con lo stesso blocco
di testo in chiaro, le loro versioni cifrate inizieranno con gli stessi blocchi di testo cifrato.
Nella realtà, spesso i messaggi iniziano con blocchi simili o identici, e non vogliamo che
un attaccante sia in grado di notarlo.

4.3.2 IV a contatore
Un’idea alternativa che si incontra a volte prevede l’utilizzo di un contatore per l’IV. Si
usa IV = 0 per il primo messaggio, IV = 1 per il secondo messaggio e così via. Anche
questa però non è una grande idea. Come si è già detto, molti messaggi reali iniziano in
maniera simile. Se i primi blocchi dei messaggi presentano semplici differenze, allora il
semplice IV a contatore potrebbe annullare le differenze nello xor e produrre nuovamente blocchi identici di testo cifrato. Per esempio: i valori 0 e 1 differiscono esattamente
in un bit; se i blocchi iniziali del testo in chiaro si distinguono solo per questo bit (e
questo capita più spesso di quanto ci si possa immaginare), allora i blocchi iniziali del
testo cifrato dei due messaggi saranno identici. L’attaccante potrà velocemente trarre
delle conclusioni sulle differenze tra i due messaggi, cosa che uno schema di cifratura
sicuro non dovrebbe consentire.

4.3.3 IV casuale
I problemi presenti nella modalità ECB a IV fisso o nella modalità CBC a IV a contatore
derivano entrambi dal fatto che i messaggi di testo in chiaro sono altamente non casuali;
molto spesso hanno un valore di intestazione fisso, o una struttura molto prevedibile.
L’attaccante di un determinato testo in chiaro potrebbe addirittura esercitare il controllo sulla struttura di quest’ultimo. Nella modalità CBC, i blocchi del testo cifrato sono
utilizzati per “casualizzare” i blocchi del testo in chiaro, ma per il primo blocco si deve
utilizzare l’IV. Questo suggerisce che si dovrebbe scegliere un IV casuale.
Si arriva così a un altro problema. Il destinatario del messaggio deve conoscere l’IV. La
soluzione standard consiste nello scegliere un IV casuale e inviarlo come primo blocco
prima del resto del messaggio cifrato. La cifratura che ne risulta ha questo schema:
C0 := valore di blocco casuale
Ci := E(K, Pi ⊕ Ci–1) per i = 1, ... , k
tenendo presente che il testo in chiaro (riempito) P1, ... , Pk viene cifrato in C0, ... , Ck.
Notate che il testo cifrato inizia da C0 e non da C1; il testo cifrato è più lungo di un
blocco rispetto al testo in chiaro. La procedura di decifratura corrispondente può essere
ricavata facilmente:
Pi := D(K, Ci) ⊕ Ci–1 per i = 1, ... , k
Il principale svantaggio di un IV casuale è che il testo cifrato è più lungo di un blocco
rispetto al testo in chiaro. Per messaggi brevi questo produce una significativa espansione
degli stessi, che risulta sempre sgradita.

Le modalità dei cifrari a blocchi    61

4.3.4 IV generati da nonce
Un’altra soluzione al problema dell’IV è costituita da due fasi. In primo luogo, a ogni
messaggio destinato a essere cifrato con questa chiave viene assegnato un numero univoco
chiamato nonce. Il termine è la contrazione di “number used once” (numero usato solo
una volta). La proprietà fondamentale di un nonce è l’unicità: non si deve mai usare lo
stesso nonce due volte con la stessa chiave. Generalmente il nonce è un numero di messaggio di qualche tipo, meglio se combinato con qualche altra informazione. I numeri
di messaggio sono già disponibili nella gran parte dei sistemi, dato che sono usati per
mantenere i messaggi nel giusto ordine, individuare eventuali duplicati e così via. Non
è necessario che il nonce sia segreto, ma può essere usato una sola volta.
L’IV necessario per la cifratura CBC viene generato cifrando il nonce. In uno scenario
tipico, il mittente numera i messaggi consecutivamente e inserisce il numero di messaggio
in ogni trasmissione. Per inviare un messaggio si dovrebbe procedere come segue.
1. Attribuire un numero al messaggio. Tipicamente, il numero di messaggio viene assegnato da un contatore che parte da 0. Notate che al contatore non dovrebbe mai
essere consentito di ripartire da 0, perché in tale modo verrebbe distrutta la proprietà
dell’unicità.
2. Usare il numero di messaggio per costruire un nonce univoco. Per una data chiave, il nonce dovrebbe rimanere univoco nell’intero sistema, non solo su un dato
computer. Per esempio, se la stessa chiave viene usata per cifrare il traffico in due
direzioni, allora il nonce dovrebbe essere costituito dal numero di messaggio più
un’indicazione della direzione in cui il messaggio è indirizzato. Il nonce dovrebbe
avere le dimensioni di un singolo blocco del cifrario.
3. Cifrare il nonce con il cifrario a blocchi per generare l’IV.
4. Cifrare il messaggio nella modalità CBC usando l’IV.
5. Aggiungere al testo cifrato le informazioni sufficienti per consentire al ricevente di
ricostruire il nonce.Tipicamente questo comporta l’inserimento del numero di messaggio appena prima del testo cifrato, o l’utilizzo di un metodo di trasporto affidabile
per comunicare il testo cifrato, nel qual caso il numero di messaggio potrebbe essere
implicito. Il valore dell’IV (C0 nelle nostre equazioni) non deve essere trasmesso.
L’ulteriore quantità di informazioni che deve essere inserita nel messaggio in genere
è molto più piccola rispetto al caso dell’IV casuale. Per la maggior parte dei sistemi è
sufficiente un contatore di messaggi di 32 – 48 bit, rispetto al sovraccarico di 128 bit
che si avrebbe con l’IV casuale. La maggior parte dei sistemi di comunicazione reali
ha comunque bisogno di un contatore dei messaggi, o utilizza un metodo di trasporto
affidabile con un contatore implicito, per cui la soluzione dell’IV generato non produce
sovraccarico.
Se un attaccante ha il controllo completo del nonce, allora il nonce dovrebbe essere cifrato con una chiave separata quando si genera l’IV. Qualsiasi sistema reale, però, avrebbe
comunque bisogno di assicurare l’unicità del nonce, e perciò non consentirebbe scelte
arbitrarie del nonce stesso. Pertanto, nella maggioranza dei casi utilizzeremmo, per cifrare
il nonce, la stessa chiave usata per la cifratura del messaggio stesso.

62   Capitolo 4

4.4 OFB
Finora in tutte le modalità descritte si è preso il messaggio e lo si è cifrato applicando
in qualche modo il cifrario ai blocchi del messaggio stesso. La modalità OFB (Output
FeedBack) è diversa per il fatto che il messaggio stesso non viene mai utilizzato come
input per il cifrario a blocchi. Invece, il cifrario a blocchi viene utilizzato per generare un
flusso pseudocasuale di byte (chiamato key stream, o flusso chiave), di cui si esegue l’xor
con il testo in chiaro per produrre il testo cifrato. Uno schema di cifratura che genera
un simile flusso casuale di chiavi viene definito cifrario a flusso. C’è chi pensa che i cifrari
a flusso presentino degli aspetti negativi, ma non è affatto così. I cifrari a flusso sono
estremamente utili e svolgono molto bene il loro lavoro; richiedono solamente un poco
di attenzione nell’utilizzo. L’abuso di un cifrario a flusso, specialmente se si riutilizza un
nonce, può portare molto facilmente a un sistema insicuro. Una modalità come CBC è
più robusta, nel senso che continua a fare un buon lavoro anche in caso di abuso.Tuttavia,
i vantaggi dei cifrari a flusso sono spesso ancora maggiori rispetto agli svantaggi.
La modalità OFB è definita come segue:
K0 := IV
Ki := E(K, Ki−1) per i = 1, ... , k
Ci := Pi ⊕ Ki
Anche qui c’è un IV K0 che viene utilizzato per generare il flusso chiave K1, ... , Kk
cifrando ripetutamente il valore. Quindi si esegue l’xor del flusso chiave con il testo in
chiaro per generare il testo cifrato.
Il valore dell’IV deve essere casuale, e come avviene con la modalità CBC, può essere
generato in maniera casuale e trasmesso con il testo cifrato (cfr. Paragrafo 4.3.3), oppure
essere generato da un nonce (cfr. Paragrafo 4.3.4).
Un vantaggio della modalità OFB è che la decifratura coincide esattamente con la cifratura, il che consente di risparmiare nelle attività di implementazione. Particolarmente
utile è il fatto che si ha bisogno soltanto della funzione di cifratura del cifrario a blocchi,
per cui non si rende neppure necessario implementare la funzione di decifratura.
Un secondo vantaggio è che non c’è alcun bisogno di riempimento. Se pensate al flusso
chiave come a una sequenza di byte, potete utilizzare un numero di byte corrispondente alla lunghezza del messaggio. In altre parole, se l’ultimo blocco di testo in chiaro è
riempito solo in parte, vengono inviati solo i byte del testo cifrato che corrispondono
ai reali byte del testo in chiaro. La mancanza del riempimento riduce il carico di lavoro,
cosa molto importante soprattutto per i messaggi di piccole dimensioni.
La modalità OFB rivela anche il problema insito nell’utilizzo di un cifrario a flusso. Qualora venisse usato lo stesso IV per due messaggi differenti, questi verrebbero cifrati con
lo stesso flusso chiave, e questo è davvero un male. Supponiamo di avere due messaggi, i
testi in chiaro P e P'i, e che essi siano stati cifrati usando lo stesso flusso chiave, ottenendo
rispettivamente i testi cifrati C e C'. L’attaccante ora può calcolare Ci ⊕ C'i = Pi ⊕ Ki ⊕
P'i ⊕ Ki = Pi ⊕ P'i. In altre parole, è in grado di calcolare la differenza tra i due testi in
chiaro. Ora supponete che l’attaccante conosca già uno dei due testi in chiaro (cosa che
nella realtà capita molto spesso); a questo punto diventa banale calcolare l’altro testo in
chiaro. Esistono anche attacchi ben noti che ricavano le informazioni su testi in chiaro
sconosciuti in base alle differenze presenti tra essi [66].

Le modalità dei cifrari a blocchi    63

La modalità OFB presenta un ulteriore problema: se siete sfortunati, ripeterete un valore
di blocco chiave, dopo il quale la sequenza dei blocchi di chiave semplicemente si ripete.
In un singolo messaggio di grandi dimensioni, potreste avere la sfortuna di finire in un
ciclo di valori di blocchi chiave. Oppure l’IV per un messaggio potrebbe essere uguale a
quello di un blocco chiave a metà della strada verso il secondo messaggio, nel qual caso
i due messaggi useranno il medesimo flusso chiave per parte dei loro testi in chiaro. In
ogni caso, finireste per cifrare blocchi di messaggio diversi con lo stesso blocco chiave,
e questo non rappresenta uno schema di cifratura sicuro.
È necessario cifrare una grande quantità di dati prima che il caso descritto diventi plausibile. In sostanza si tratta di un attacco di collisione tra i blocchi del flusso chiave e i punti
di avvio iniziali, per cui si tratta di cifrare almeno 264 blocchi di dati prima di potersi
attendere una simile collisione. Questo è un esempio del motivo per cui un cifrario con
blocchi di 128 bit può offrire solo 64 bit di sicurezza. Se si limita la quantità di dati che
possono essere cifrati con ciascuna chiave, si può ridurre la probabilità di ripetizione di
un valore di blocco chiave. Sfortunatamente il rischio rimane sempre, e nei casi sfortunati
si rischia di perdere la riservatezza di un intero messaggio.

4.5 CTR
Un’altra modalità di cifrario a blocchi è la modalità CTR (CounTeR), o contatore. Pur
essendo in circolazione da molto tempo, non è mai stata standardizzata come modalità DES ufficiale [95], e per questo motivo è spesso trascurata nei libri. Il NIST ne ha
recentemente effettuato la standardizzazione [40]. Come la modalità OFB, la modalità
CTR è un cifrario a flussi. È definita come segue:
Ki := E(K, Nonce || i) per i = 1, ... , k
Ci := Pi ⊕ Ki
Come qualsiasi cifrario a flussi, richiede che venga fornito un nonce univoco di qualche
tipo. La maggior parte dei sistemi costruisce il nonce con un numero di messaggio e
alcuni dati aggiuntivi per assicurarne l’univocità.
La modalità CTR utilizza un metodo assai semplice per generare il flusso chiave: concatena il nonce con il valore del contatore e lo cifra a formare un singolo blocco del flusso
chiave. Questo richiede che il contatore e il nonce possano essere contenuti entrambi
in un singolo blocco, ma con le dimensioni odierne dei blocchi a 128 bit, i problemi a
questo riguardo sono rari. Ovviamente il nonce deve essere più piccolo di un blocco,
dato che serve lo spazio per il valore del contatore i. Una struttura tipica potrebbe usare
un numero di messaggio di 48 bit, 16 bit di dati nonce aggiuntivi e 64 bit per il contatore
i. Questo limita la capacità di cifratura del sistema a 248 messaggi diversi utilizzando una
sola chiave, e limita le dimensioni di ciascun messaggio a 268 byte.
Come per la modalità OFB, si deve essere assolutamente certi di non riutilizzare mai una
singola combinazione chiave/nonce. Questo è uno svantaggio che viene spesso menzionato in riferimento alla modalità CTR, ma anche la modalità CBC presenta esattamente
lo stesso problema. Se viene usato per due volte lo stesso IV, iniziano le perdite di dati sui
testi in chiaro. La modalità CBC è un poco più robusta, dato che è più verosimilmente
in grado di limitare la quantità di dati rivelati. Ma qualsiasi fuga di informazioni viola i

64   Capitolo 4

nostri requisiti, e in una struttura modulare non si può fare affidamento sul fatto che il
resto del sistema sia in grado di limitare i danni nel caso in cui venga rivelata anche una
piccola quantità di informazioni. Quindi, sia con la modalità CBC sia con la modalità
CTR è necessario avere la certezza che il nonce o l’IV sia unico.
La vera questione è se si possa assicurare l’unicità del nonce. In caso di dubbi si dovrebbe
utilizzare una modalità come la CBC a IV casuale, dove l’IV viene generato in maniera
casuale e fuori dal controllo di chi ha sviluppato l’applicazione. Se invece è possibile
garantire che il nonce sarà unico, allora la modalità CTR offre una grande facilità di
impiego. È sufficiente implementare la funzione di cifratura del cifrario a blocchi, e le
funzioni di cifratura e decifratura sono identiche. È molto semplice accedere a parti
arbitrarie del testo in chiaro, dato che ogni blocco del flusso chiave può essere calcolato immediatamente. Per applicazioni che richiedono alta velocità, il calcolo del flusso
chiave può essere parallelizzato a un livello arbitrario. Inoltre, la sicurezza della modalità
CTR è legata semplicemente alla sicurezza del cifrario a blocchi. Ogni punto debole
della modalità di cifratura CTR implica immediatamente un attacco con testo in chiaro
scelto sul cifrario a blocchi. L’inverso logico di tutto ciò è che, se non si ha un attacco
sul cifrario a blocchi, allora non vi è attacco sulla modalità CTR (a parte l’analisi del
traffico e la fuga di informazioni di cui ci occuperemo tra breve).

4.6 Cifratura e autenticazione insieme
Tutte le modalità esaminate finora risalgono alla fine degli anni ’70 e ai primi anni ’80.
Negli ultimi anni sono state proposte alcune modalità di cifrari blocchi nuove. Il NIST
di recente ha deciso di standardizzarne due, denominate CCM [41] e GCM [43]. Queste
modalità offrono sia autenticazione che cifratura. Le prenderemo in esame nel Capitolo
7, dopo aver parlato dell’autenticazione.

4.7 Quale modalità utilizzare?
Abbiamo discusso diverse modalità, ma in realtà sono solo due quelle che potremmo
pensare di utilizzare: CBC e CTR. Abbiamo già spiegato che la modalità ECB non è
abbastanza sicura. La modalità OFB è una buona scelta, ma la modalità CTR è migliore
per certi aspetti e non soffre del problema del ciclo breve. Non c’è un motivo per preferire la modalità OFB alla modalità CTR.
Dunque sarebbe meglio usare la modalità CBC o la modalità CTR? Nella prima edizione di questo libro avevamo raccomandato la modalità CTR. Tuttavia, col tempo si
impara sempre di più e ora raccomandiamo la modalità CBC con IV casuale. Perché
questo cambiamento? Abbiamo visto troppe applicazioni che sono insicure perché non
generano il nonce nel modo corretto. La modalità CTR è una scelta molto buona, ma
solo se l’applicazione può garantire che il nonce sia unico, anche se il sistema è sotto
attacco. Ciò finisce per rivelarsi una fonte di problemi e falle di sicurezza. La modalità
CBC con IV casuale presenta alcuni svantaggi (il testo cifrato è più lungo, il testo in
chiaro ha bisogno del riempimento e il sistema ha bisogno di un generatore di numeri
casuali), ma è robusta e sopporta bene gli abusi. La generazione del nonce si rivela un
problema davvero difficile in molti sistemi, perciò non raccomandiamo di presentare

Le modalità dei cifrari a blocchi    65

agli sviluppatori di applicazioni una modalità che ne faccia uso. Questo vale anche per la
versione della modalità CBC con IV generato tramite nonce. Quindi, se state sviluppando
un’applicazione e dovete usare una modalità di cifratura, andate sul sicuro e scegliete la
modalità CBC con IV casuale.
Tenete sempre presente che una modalità di cifratura fornisce solo la riservatezza.
Ovvero, l’attaccante non è in grado di trovare alcuna informazione sui dati che state
comunicando, a parte il fatto che state comunicando, quando state comunicando, quanto
state comunicando e con chi state comunicando. L’analisi di questo tipo di informazioni
esterne viene chiamata analisi del traffico e può fornire a un attaccante informazioni molto
utili. Impedire l’analisi del traffico è possibile, ma in genere troppo oneroso in termini
di larghezza di banda per chiunque eccetto le forze armate.
Ricordate inoltre che le modalità di cifratura descritte in questo capitolo sono pensate solo
per garantire la riservatezza contro chi cerca di spiarvi; non impediscono a un attaccante di
modificare i dati. Parleremo di come proteggere riservatezza e autenticità nel Capitolo 7.

4.8 Fuga di informazioni
Siamo finalmente giunti all’oscuro segreto delle modalità dei cifrari a blocchi. Tutti i
cifrari a blocchi lasciano filtrare una certa quantità di informazioni.
Per questa discussione partiremo dal presupposto di avere a disposizione un cifrario a
blocchi perfetto.Tuttavia, anche con un cifrario a blocchi perfetto i testo cifrati prodotti
dalle modalità di cifratura rivelano informazioni sui testi in chiaro. Ciò ha a che fare con
le uguaglianze o disuguaglianze tra blocchi di testo cifrato e blocchi di testo in chiaro.
Cominciamo con la modalità ECB. Se due blocchi di testo in chiaro sono uguali (Pi = Pj),
allora anche i blocchi di testo cifrato saranno uguali (Ci = Cj). Per i testi in chiaro casuali
questo si verificherà molto di rado, ma la maggior parte dei testi in chiaro non è casuale,
bensì altamente strutturata. Così blocchi uguali di testo in chiaro si presentano molto
più spesso di quelli casuali, e i blocchi uguali di testo cifrato rivelano questa struttura.
Questo è il motivo che ci ha portato a scartare la modalità ECB.
E per quanto riguarda la modalità CBC? Blocchi uguali di testo in chiaro non portano
a blocchi uguali di testo cifrato, dato che ogni blocco di testo in chiaro subisce innanzitutto un xor con il blocco precedente di testo cifrato, prima di essere cifrato. Pensate
a tutti i blocchi di testo cifrato come a valori casuali; dopo tutto, sono stati generati da
un cifrario a blocchi che produce un output casuale per ogni determinato input. Ma se
abbiamo due blocchi di testo cifrato uguali? Abbiamo:
Ci = Cj
E(K, Pi ⊕ Ci−1) = E(K, Pj ⊕ Cj−1)
Pi ⊕ Ci−1 = Pj ⊕ Cj−1
Pi ⊕ Pj = Ci−1 ⊕ Cj−1

dalla specifica della modalità CBC
decifra entrambi i membri
algebra di base

L’ultima equazione fornisce la differenza tra due blocchi di testo in chiaro come xor di
due blocchi di testo cifrato, che supponiamo noti all’attaccante. Non è di certo qualcosa
che ci si aspetterebbe da un sistema di cifratura dei messaggi perfetto. E se il testo in
chiaro presenta molte ridondanze, come nel caso di un testo in italiano, probabilmente
conterrà informazioni sufficienti per recuperare entrambi i blocchi di testo in chiaro.

66   Capitolo 4

Una situazione simile si verifica quando due testi cifrati sono diversi. Sapere che Ci ≠ Cj
implica che Pi ⊕ Pj ≠ Ci−1 ⊕ Cj−1, per cui ogni coppia diseguale di testi cifrati porta a
una formula di disuguaglianza tra i blocchi di testo in chiaro.
La modalità CTR ha proprietà simili. Con questa modalità di cifratura sappiamo che
i blocchi Ki sono tutti diversi, perché sono le cifrature di un nonce e di un valore di
contatore.Tutti i valori di testo in chiaro della cifratura sono diversi, per cui tutti i valori
del testo cifrato (che formano i blocchi chiave) sono diversi. Dati due testi cifrati Ci e
Cj, si sa che Pi ⊕ Pj ≠ Ci ⊕ Cj perché altrimenti i due blocchi di flusso chiave sarebbero
dovuti essere uguali. In altre parole, la modalità CTR fornisce una disuguaglianza di testi
in chiaro per ciascuna coppia di blocchi di testo cifrato.
Non ci sono problemi di collisione nella modalità CTR. Due blocchi chiave non sono
mai uguali, e blocchi di testo in chiaro uguali o blocchi di testo cifrato uguali non hanno
conseguenze. L’unico aspetto che fa deviare la CTR rispetto all’ideale assoluto di cifrario
a flusso è l’assenza di collisioni di blocco chiave.
La modalità OFB è peggiore sia della modalità CBC che della modalità CTR. Finché
non vi sono collisioni sui blocchi di flusso chiave, la modalità OFB fa trapelare la stessa
quantità di informazioni della modalità CTR. Ma se si verifica una collisione di due
blocchi di flusso chiave, allora anche tutti i blocchi di flusso chiave successivi produrranno
una collisione; dal punto di vista della sicurezza è un disastro, e questa è una delle ragioni
per cui la modalità CTR è preferibile alla modalità OFB.

4.8.1 Probabilità di una collisione
Quali sono le possibilità che due blocchi di testo cifrato siano uguali? Supponiamo di
cifrare in totale M blocchi. Non importa se lo facciamo con pochi messaggi di grandi
dimensioni o molti messaggi di piccole dimensioni, ciò che conta è il numero totale di
blocchi. Una stima con una buona approssimazione è che ci sono M(M − 1)/2 coppie
di blocchi, e per ogni coppia vi è una probabilità 2−n di uguaglianza, dove n è la dimensione del blocco del cifrario a blocchi. Quindi il numero atteso di blocchi uguali di
testo cifrato è M(M − 1)/2n+1, che si avvicina all’unità quando M ≈ 2n/2. In altre parole,
quando si cifrano circa 2n/2 blocchi, ci si può aspettare di ottenere due blocchi di testo
cifrato uguali. In realtà il numero di blocchi che possono essere cifrati prima di aspettarsi
il primo duplicato è più vicino a π 2n − 1 = 2n /2 π / 2 , ma la teoria alla base di questa
analisi è molto difficile e qui non ci interessa un simile livello di precisione.
Con una dimensione del blocco di n = 128 bit ci si può aspettare il primo valore duplicato
di un blocco di testo cifrato dopo circa 264 blocchi. Questo è il paradosso del compleanno
spiegato nel Paragrafo 2.7.1. Ora, 264 blocchi rappresentano una grande quantità di dati,
ma non dimenticate che stiamo progettando sistemi che dovranno durare 30 anni. Può
darsi che nel futuro ci sarà qualcuno disponibile a elaborare 264 blocchi di dati.
Anche gli insiemi di dati più piccoli sono a rischio. Se si elaborano 240 blocchi (circa
16 TB di dati) si ha una probabilità pari a 2−48 che si verifichi una collisione tra blocchi
di testo cifrato. È davvero una probabilità piccola, ma consideratela dal punto di vista
dell’attaccante. Per una particolare chiave in uso, l’attaccante raccoglie 240 blocchi e
cerca eventuali duplicati. Dato che la probabilità di trovarne uno è piccola, tutto questo
processo deve essere ripetuto per circa 248 diverse chiavi. La quantità totale di lavoro
necessaria prima di trovare una collisione è di 240 · 248 = 288, molto inferiore alla nostra
forza di progetto di 128 bit.

Le modalità dei cifrari a blocchi    67

Concentriamoci sulle modalità CBC e CTR. Nella modalità CTR si ottiene una disuguaglianza di testi in chiaro per ogni coppia di blocchi. Nella CBC si ottiene una disuguaglianza
se i due blocchi di testo cifrato sono diversi e un’uguaglianza se i blocchi sono uguali.
Ovviamente un’uguaglianza fornisce a un attaccante molte più informazioni sul testo in
chiaro di una disuguaglianza, per cui la modalità CTR lascia filtrare meno informazioni.

4.8.2 Come gestire le fughe di informazioni
Come possiamo raggiungere il nostro obiettivo di un livello di sicurezza di 128 bit? In
sostanza, non possiamo raggiungerlo, ma possiamo arrivare il più vicino possibile. Non
esiste una via facile per ottenere un livello di sicurezza di 128 bit con un cifrario a blocchi la cui dimensione del blocco è di 128 bit. Questo è il motivo per cui si vorrebbero
cifrari a blocchi con blocchi da 256 bit, ma non sono ancora in circolazione proposte
ben studiate di questo tipo, quindi siamo in un vicolo cieco. Quello che possiamo fare
è avvicinarci al livello di sicurezza desiderato e limitare i danni.
La modalità CTR lascia filtrare una quantità molto piccola di dati. Supponiamo di cifrare 264 blocchi di dati e generare un testo cifrato C. Per ogni possibile testo in chiaro
P della lunghezza di 264 blocchi, l’attaccante può calcolare il flusso chiave che dovrebbe
venire utilizzato per cifrare questo P in C. C’è una probabilità del 50% circa che il flusso
chiave risultante conterrà una collisione. Sappiamo che la modalità CTR non produce mai collisioni, perciò, se si verifica una collisione, quel particolare testo in chiaro P
può essere escluso. Ciò significa che l’attaccante può escludere approssimativamente la
metà di tutti i possibili testi in chiaro. Questo corrisponde a rivelare all’attaccante un
singolo bit di informazioni. Anche rivelare un singolo bit di informazioni a volte può
costituire un problema. Tuttavia, lasciare trapelare un singolo bit di informazioni per
264 blocchi non è molto. Se ci limitiamo a cifrare solo 248 blocchi, allora l’attaccante
potrà escludere approssimativamente 2−32 di tutti i testi in chiaro, una quantità ancora
minore. In una situazione concreta, una perdita così ridotta è insignificante se vista nel
contesto dei requisiti di un attacco. Pertanto, anche se la cifratura CTR non è perfetta, è
possibile limitare i danni a una perdita estremamente piccola evitando di cifrare troppe
informazioni con la stessa chiave. Sarebbe ragionevole limitare la modalità di cifratura a
260 blocchi, quantità che consente di cifrare 264 byte ma riduce la perdita a una piccola
frazione di un bit.
Utilizzando la modalità CBC si dovrebbe essere un poco più restrittivi. Se nella modalità
CBC si verifica una collisione, vengono svelati 128 bit di informazioni sul testo in chiaro.
È una buona politica mantenere bassa la probabilità di una simile collisione. Suggeriamo di
limitare la cifratura CBC a 232 blocchi o a una quantità simile. Questo lascia una probabilità
residua di 2−64 di rivelare 128 bit, il che è probabilmente inoffensivo per la maggior parte
delle applicazioni, ma sicuramente lontano dal livello di sicurezza desiderato.
Solo un promemoria: questi limiti sono relativi alla quantità totale di informazioni cifrate
con una sola chiave. Non importa se i dati cifrati stanno tutti in un solo messaggio di
grandi dimensioni o in un gran numero di messaggi di dimensioni inferiori.
Non siamo in una situazione soddisfacente, ma è quella in cui ci troviamo. La cosa migliore che possiamo fare a questo punto è usare la modalità CTR o la modalità CBC
e limitare la quantità di dati elaborata con una qualsiasi chiave. Più avanti parleremo
dei protocolli di negoziazione delle chiavi. È piuttosto semplice impostare una nuova
chiave quando la vecchia sta per raggiungere il suo limite di utilizzo. Supponendo che

68   Capitolo 4

stiate già utilizzando un protocollo di negoziazione delle chiavi per impostare la chiave
di cifratura, rinnovare una chiave non è particolarmente difficile. È una complicazione,
ma ben motivata.

4.8.3 La matematica com’è trattata in questo libro
I lettori con una formazione matematica potrebbero inorridire davanti al nostro uso
sconsiderato delle probabilità senza una verifica della loro indipendenza. Hanno ragione,
naturalmente, se si considera il tutto da un punto di vista puramente matematico. Ma
come i fisici, i crittografi usano la matematica in un modo che hanno trovato utile. I valori
crittografici tipicamente si comportano in maniera molto casuale. Dopo tutto, i crittografi
fanno di tutto per distruggere completamente tutti gli schemi, dato che qualsiasi schema
può portare a un attacco. L’esperienza mostra che questo modo di trattare le probabilità
porta a risultati piuttosto accurati. I matematici sono benvenuti se intendono lavorare sui
dettagli per verificare i risultati precisi in modo da soddisfare la loro curiosità, noi però
preferiamo utilizzare approssimazioni più sommarie, per la loro semplicità.

4.9 Esercizi
Esercizio 4.1 Sia P un testo in chiaro e ℓ(P) la lunghezza in byte di P. Sia b la dimensione in byte del blocco del cifrario a blocchi. Spiegate perché il seguente non è un
buono schema di riempimento: determinare il numero minimo di byte di riempimento
necessari per riempire il testo in chiaro fino a un limite di blocco. Questo è un numero
n che soddisfa la condizione 0 ≤ n ≤ b − 1 e n + ℓ(P) è un multiplo di b. Riempite il
testo in chiaro aggiungendo n byte, ognuno di valore n.
Esercizio 4.2 Confrontate i vantaggi e gli svantaggi in termini di sicurezza e prestazioni di ognuna delle varianti della modalità CBC descritte nel capitolo: con IV fisso,
con IV a contatore, con IV casuale, con IV generato da nonce.
Esercizio 4.3 Supponete di essere un attaccante e di vedere il seguente testo cifrato
di 32 byte C (in esadecimale):
46 64 DC 06 97 BB FE 69 33 07 15 07 9B A6 C2 3D
2B 84 DE 4F 90 8D 7D 34 AA CE 96 8B 64 F3 DF 75

E il seguente testo cifrato di 32 bit (in esadecimale):
51 7E CC 05 C3 BD EA 3B 33 57 0E 1B D8 97 D5 30
7B D0 91 6B 8D 82 6B 35 B7 8B BB 8D 74 E2 C7 3B

Supponete di sapere che questi testi cifrati sono stati generati usando la modalità CTR
con lo stesso nonce. Il nonce è implicito, per cui non è inserito nel testo cifrato. Sapete
anche che il testo cifrato P corrispondente a C è:
43 72 79 70 74 6F 67 72 61 70 68 79 20 43 72 79
70 74 6F 67 72 61 70 68 79 20 43 72 79 70 74 6F

Che informazioni potete eventualmente ricavare sul testo in chiaro P' corrispondente
a C'?

Le modalità dei cifrari a blocchi    69

Esercizio 4.4 Il testo cifrato (in esadecimale):
87 F3 48 FF 79 B8 11 AF 38 57 D6 71 8E 5F 0F 91
7C 3D 26 F7 73 77 63 5A 5E 43 E9 B5 CC 5D 05 92
6E 26 FF C5 22 0D C7 D4 05 F1 70 86 70 E6 E0 17

è stato generato con la chiave AES a 25 bit (in esadecimale):
80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01

usando la modalità CBC con un IV casuale. L’IV è inserito all’inizio del messaggio.
Decifrate il testo cifrato. Per questo esercizio potete utilizzare una libreria crittografica
esistente.
Esercizio 4.5 Cifrate il testo in chiaro:
62 6C 6F 63 6B 20 63 69 70 68 65 72 73 20 20 20
68 61 73 68 20 66 75 6E 63 74 69 6F 6E 73 20 78
62 6C 6F 63 6B 20 63 69 70 68 65 72 73 20 20 20

usando l’AES in modalità ECB e la chiave:
80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01

Per questo esercizio potete utilizzare una libreria crittografica esistente.
Esercizio 4.6 Sia P1, P2 un messaggio lungo due blocchi, sia P'1 un messaggio lungo
un blocco. Sia C0, C1, C2 la cifratura di P1, P2 usando la modalità CBC con un IV casuale e una chiave casuale, e sia C'0, C'1 la cifratura di P'1 usando la modalità CBC con
un IV casuale e la medesima chiave. Supponete che un attaccante conosca P1, P2 e che
abbia intercettato e quindi conosca C0, C1, C2 e C'0, C'1. Supponete ancora che per una
possibilità a caso, C'1 = C2. Mostrate che l’attaccante è in grado di calcolare P'1.

Capitolo 5

Le funzioni di hash

Una funzione di hash accetta come input una stringa
di bit (o byte) di lunghezza arbitraria e produce un
risultato di dimensione fissa. Un impiego tipico delle
funzioni di hash è rappresentato dalle firme digitali.
Dato un messaggio m, si potrebbe firmare direttamente il messaggio, tuttavia le operazioni a chiavi
pubbliche della maggior parte degli schemi di firme
digitali sono piuttosto esose in termini computazionali; quindi, anziché firmare direttamente m, si applica
una funzione di hash h e si firma h(m). Il risultato di
h ha tipicamente lunghezza compresa tra 128 e 1024
bit, molto meno rispetto alle migliaia di milioni di bit
che potrebbe raggiungere la lunghezza del messaggio
m. Firmare h(m) risulta pertanto molto più veloce che
firmare direttamente m. Perché questa procedura sia
sicura, deve essere impossibile comporre due messaggi m1 e m2 che sottoposti ad hash producano lo
stesso valore. Discuteremo di seguito i dettagli della
sicurezza delle funzioni di hash.
Le funzioni di hash vengono anche chiamate funzioni
di message digest o di sintesi, e il risultato dell’operazione di hash è chiamato anche digest, fingerprint
o impronta digitale. Preferiamo usare il termine più
comune funzioni di hash, dato che gli impieghi di
queste funzioni non sono limitati alla sintesi. Un
avvertimento doveroso: il termine funzione di hash
viene utilizzato anche per la funzione di mappatura
usata nell’accesso alle tabelle di hash, strutture dati
utilizzate in molti algoritmi. Queste cosiddette funzioni di hash hanno proprietà simili alle funzioni di
hash utilizzate nella crittografia, ma tra i due tipi c’è
un’enorme differenza: le funzioni di hash usate nella
crittografia hanno proprietà di sicurezza specifiche,

Sommario
5.1 Sicurezza delle funzioni
di hash
5.2 Funzioni di hash reali
5.3 Vulnerabilità delle
funzioni di hash
5.4 Rimediare ai punti deboli
5.5 Quale funzione di hash
scegliere?
5.6 Esercizi

72   Capitolo 5

mentre la funzione di mappatura delle tabelle di hash ha requisiti molto più deboli.
State attenti a non confonderle. In questo libro, quando si parla di funzioni di hash, ci si
riferisce sempre alle funzioni crittografiche.
Le funzioni di hash trovano molte applicazioni nella crittografia. Costituiscono un ottimo
collante tra parti diverse di un sistema crittografico. In molti casi, quando si ha un valore
di dimensioni variabili, è possibile usare una funzione di hash per farlo corrispondere a
un valore a dimensione fissa. Le funzioni di hash possono essere utilizzate nei generatori
di numeri pseudocasuali crittografici per generare svariate chiavi da un singolo segreto
condiviso. Inoltre presentano una proprietà di non reversibilità che isola le diverse parti
di un sistema, assicurando che anche qualora un attaccante riesca a conoscere un valore,
non arriverà a conoscere gli altri.
Benché le funzioni di hash vengano utilizzate in quasi tutti i sistemi, nella comunità della
crittografia attualmente le conosciamo meno dei cifrari a blocchi. Fino a poco tempo
fa gli studi svolti sulle funzioni di hash erano minori rispetto a quelli concentrati sui
cifrari a blocchi, e non erano presenti molte proposte concrete tra cui scegliere. Questa
situazione sta cambiando. Il NIST è in fase di scelta di un nuovo standard per la funzioni
di hash, che si chiamerà SHA-3. Il processo di selezione della funzione SHA-3 si sta
dimostrando molto simile a quello che portò a individuare AES come il nuovo standard
per i cifrari a blocchi.

5.1 Sicurezza delle funzioni di hash
Come si è accennato in precedenza, una funzione di hash trasforma un input m in un
output a dimensione fissa h(m). Le dimensioni di output tipiche variano tra 128 e 1024
bit. Potrebbe esserci un limite sulla lunghezza dell’input, ma per tutti gli scopi concreti
l’input può avere lunghezza arbitraria. Una funzione di hash deve soddisfare diversi
requisiti; il più semplice prevede che debba essere non reversibile: dato un messaggio m è
semplice calcolare h(m), ma dato un valore x non è possibile individuare un m tale che
h(m) = x. In altre parole, una funzione non reversibile è una funzione che può essere
calcolata ma non può essere invertita.
Tra le molte proprietà che una buona funzione di hash dovrebbe possedere, quella menzionata con più frequenza è la resistenza alle collisioni. Una collisione si verifica quando
ci sono due input m1 e m2 per i quali h(m1) = h(m2). Ovviamente, ogni funzione di hash
presenta un numero infinito di queste collisioni (si ha un numero infinito di possibili
valori di input e un numero finito di possibili valori di output). Pertanto, una funzione
di hash non è mai immune dalle collisioni. Il requisito della resistenza alle collisioni
afferma semplicemente che le collisioni, anche se esistono, non devono poter essere
individuate.
La resistenza alle collisioni è la proprietà che rende le funzioni di hash adatte all’uso
negli schemi per le firme. Tuttavia, esistono funzioni di hash resistenti alle collisioni che
sono del tutto inadatte per molte altre applicazioni, come la derivazione delle chiavi, le
funzioni unidirezionali e così via. In sostanza, i progettisti dei sistemi di crittografia si
aspettano che una funzione di hash sia una mappatura casuale. Ogni altra definizione
porta a una situazione in cui chi progetta non può più trattare la funzione di hash come

Le funzioni di hash   73

una scatola nera ideale, ma deve valutare il modo in cui le sue proprietà interagiscono
con il sistema che la circonda. Riportiamo di seguito alcune definizioni (in questo libro
le definizioni sono numerate a livello globale e non capitolo per capitolo).
Definizione 4
La funzione di hash ideale si comporta come una mappatura ideale da tutti i possibili
valori di input sull’insieme di tutti i possibili valori di output.

Come la nostra definizione del cifrario a blocchi ideale (Paragrafo 3.3), questa è una
definizione incompleta. A rigor di termini non esiste una mappatura casuale; si può
soltanto parlare di una distribuzione di probabilità su tutte le possibili mappature. Per i
nostri scopi, comunque, questa definizione è accettabile.
Ora siamo in grado di definire che cosa sia un attacco a una funzione di hash.
Definizione 5
Un attacco a una funzione di hash è un metodo non generico di distinguere la funzione
di hash da una funzione di hash ideale.

Qui la funzione di hash ideale deve ovviamente avere la stessa dimensione di output
della funzione sotto attacco. Come per i cifrari a blocchi, il requisito di “non genericità”
riguarda tutti gli attacchi. Le nostre osservazioni sugli attacchi generici contro i cifrari a
blocchi valgono anche per questa situazione. Per esempio, se un attacco può essere usato
per distinguere fra due funzioni di hash ideali, allora non sfrutta alcuna proprietà della
funzione di hash stessa ed è quindi un attacco generico.
Rimane da chiedersi quale sia la quantità di lavoro che l’attaccante è autorizzato a svolgere.
A differenza del cifrario a blocchi, la funzione di hash non ha una chiave, e non esiste un
attacco generico come la ricerca esaustiva della chiave. L’unico parametro interessante è
la dimensione dell’output. Un attacco generico su una funzione di hash è l’attacco del
compleanno, che genera collisioni. Per una funzione di hash con output di n bit sono
richiesti circa 2n/2 passaggi. Ma le collisioni sono rilevanti solo per determinati utilizzi delle
funzioni di hash. In altre situazioni, lo scopo è trovare una preimmagine (dato x, trovare un
m tale che h(m) = x), oppure trovare qualche tipo di struttura negli output della funzione
di hash. L’attacco generico della preimmagine richiede circa 2n passaggi. Non staremo
qui a discutere quali attacchi siano rilevanti e quanto lavoro potrebbe essere considerato
ragionevole per un particolare tipo di attacco. Per avere senso, un discriminatore deve
essere più efficiente di un attacco generico che produce risultati simili. Sappiamo che
questa non è una definizione esatta, ma – come per i cifrari a blocchi – non disponiamo
di una definizione esatta. Se qualcuno lamenta un attacco, chiedetevi semplicemente se
sarebbe possibile ottenere un risultato simile o migliore con un attacco generico che non
si basi sui dettagli della funzione di hash. Se la risposta è sì, il discriminatore è inutile. Se
la risposta è no, il discriminatore è concreto.
Come per i cifrari a blocchi, ammettiamo un livello di sicurezza ridotto, ove sia specificato.
Possiamo ipotizzare una funzione di hash a 512 bit che specifichi un livello di sicurezza
di 128 bit. In tal caso, i discriminatori avranno un limite di 2128 passaggi.

74   Capitolo 5

5.2 Funzioni di hash reali
Ci sono pochissime funzioni di hash valide in circolazione. Al momento si è sostanzialmente limitati alla famiglia SHA: SHA-1, SHA-224, SHA-256, SHA-384 e SHA-512.
Esistono altre proposte pubblicate, compresa quella per il nuovo standard SHA-3, ma tutte
devono essere esaminate con maggiore cura prima di poter essere considerate affidabili.
Perfino le funzioni presenti nella famiglia SHA non sono state analizzate a sufficienza,
ma almeno sono state standardizzate dal NIST e sviluppate dalla NSA (qualsiasi cosa si
possa pensare di NSA, finora ha pubblicato crittografia di qualità accettabile).
Quasi tutte le funzioni di hash concrete, e tutte quelle che esamineremo, sono iterative.
Le funzioni di hash iterative suddividono l’input in una sequenza di blocchi di dimensioni fisse m1, ... , mk, usando una regola di riempimento per completare l’ultimo blocco.
Una lunghezza del blocco tipica è 512 bit, e l’ultimo blocco tipicamente conterrà una
stringa che rappresenta la lunghezza dell’input. I blocchi del messaggio vengono quindi
elaborati in ordine, utilizzando una funzione di compressione e uno stato intermedio di
lunghezza fissa. Questo processo inizia con un valore fisso, H0, e definisce Hi = h (Hi−1,
mi). Il valore finale Hk è il risultato della funzione di hash.
Una modalità iterativa come questa presenta vantaggi pratici significativi. Innanzitutto è
facile da specificare e da implementare, rispetto a una funzione che gestisce direttamente
input di lunghezza variabile. Inoltre, questa struttura consente di iniziare a calcolare l’hash
di un messaggio appena si entra in possesso della sua prima parte. Perciò, nelle applicazioni dove si deve ottenere l’hash di un flusso di dati, il messaggio può essere trattato al
momento senza mai memorizzare i dati. Come per i cifrari a blocchi, non spenderemo
altro tempo spiegando le diverse funzioni di hash nei dettagli. Le specifiche contengono
molti dettagli che non rientrano negli scopi principali di questo libro.

5.2.1 Una funzione di hash semplice ma non sicura
Prima di esaminare funzioni di hash reali, comunque, presentiamo un esempio di funzione
di hash iterativa banalmente insicura, che ci aiuterà a chiarire la definizione di attacco
generico. Questa funzione di hash è costruita partendo da AES con una chiave di 256
bit. Sia K una chiave di 256 bit impostata a tutti zeri. Per calcolare l’hash del messaggio
m, se ne effettua il riempimento in qualche modo e poi lo si suddivide in blocchi da
128 bit m1, ... , mk; i dettagli dello schema di riempimento non sono importanti qui. Si
imposta H0 a un blocco di 128 bit di tutti zeri, quindi si calcola Hi = AESK(Hi−1 ⊕ mi).
Sia Hk il risultato della funzione di hash.
Questa è una funzione di hash sicura? È resistente alle collisioni? Prima di proseguire
nella lettura, provate a cercare di compromettere la funzione da soli.
Ora ecco un attacco non generico. Scegliamo un messaggio m tale che dopo il riempimento venga suddiviso in due blocchi m1 e m2. Poniamo che H1 e H2 indichino i
valori calcolati come parte delle elaborazioni interne della funzione di hash; H2 è anche
l’output della funzione di hash. Ora sia m’1 = m2 ⊕ H1 e sia m’2 = H2 ⊕ m2 ⊕ H1, e sia
m’1 il messaggio che si suddivide in m’1 e m’2 dopo il riempimento. Date la proprietà
di costruzione della funzione di hash, anche m’ viene trasformato dall’hash in H2; lo
potete verificare negli esercizi riportati al termine del capitolo. E con una probabilità
molto alta, m e m’ sono stringhe diverse. Proprio così: m e m’ sono due messaggi diversi
che producono una collisione quando vengono trattati da questa funzione di hash. Per

Le funzioni di hash   75

trasformare questo attacco in attacco discriminante, proviamo semplicemente a portarlo
contro la funzione di hash. Se l’attacco funziona, la funzione di hash è quella debole, che
abbiamo descritto qui; altrimenti è la funzione di hash ideale. Questo attacco sfrutta una
vulnerabilità specifica nel modo in cui la funzione di hash è stata progettata, pertanto è
un attacco non generico.

5.2.2 MD5
Passiamo a esaminare qualche proposta reale di funzione di hash. MD5 è una funzione
di hash a 128 bit sviluppata da Ron Rivest [104]. È l’evoluzione di una funzione denominata MD4 [106] con rinforzi aggiuntivi contro gli attacchi. MD4 è molto veloce, ma
è anche stata violata [36]. Anche MD5 oggi è stata violata, tuttavia è ancora utilizzata
in alcuni sistemi reali.
Il primo passo per calcolare MD5 è quello di suddividere il messaggio in blocchi di 512
bit. L’ultimo blocco viene riempito e viene inserita anche la lunghezza del messaggio.
MD5 ha uno stato di 128 bit che viene suddiviso in quattro parole di 32 bit ciascuna. La
funzione di compressione h' ha quattro cicli, e in ogni ciclo il blocco del messaggio e lo
stato vengono mescolati. Il mescolamento consiste in una combinazione di operazioni
di addizione, xor, and, or e rotazione su parole di 32 bit (per maggiori dettagli cfr.
[104]). Ogni ciclo mescola l’intero blocco del messaggio nello stato, così ogni parola del
messaggio viene usata in effetti quattro volte. Dopo i quattro cicli della funzione h', lo
stato dell’input e il risultato vengono sommati per produrre l’output di h'.
Questa struttura di lavoro a parole di 32 bit è molto efficiente sulle CPU a 32 bit. Sperimentata da MD4, ora è una caratteristica generica di molte primitive crittografiche.
Per la maggior parte delle applicazioni, i 128 bit di MD5 sono insufficienti. Usando il
paradosso del compleanno, possiamo facilmente trovare collisioni in qualsiasi funzione
di hash a 128 bit con 264 valutazioni della funzione di hash. Questo ci consentirebbe di
trovare reali collisioni contro MD5 usando solo 264 calcoli di MD5. E ciò non è sufficiente per i moderni sistemi.
Ma in realtà la situazione è ancora peggiore. La struttura interna di MD5 la rende
vulnerabile ad attacchi più efficienti. Una delle idee alla base della progettazione della
funzione di hash iterativa è che, se h' è resistente alle collisioni, allora la funzione di
hash h costruita a partire da questa sarà anch’essa resistente alle collisioni. Dopo tutto,
qualsiasi collisione in h può verificarsi solo a causa di una collisione in h'. Da oltre un
decennio si sa che la funzione di compressione h' di MD5 presenta delle collisioni [30].
Le collisioni per h' non implicano immediatamente una collisione per MD5, ma alcuni
recenti progressi nella crittoanalisi, a iniziare da Wang e Yu [124], hanno mostrato che è
effettivamente possibile trovare collisioni per l’intera MD5 usando molto meno di 264
calcoli di MD5. Anche se l’esistenza di simili efficienti attacchi di individuazione delle
collisioni potrebbe non compromettere tutti gli utilizzi di MD5, possiamo certamente
affermare che MD5 è molto debole e non dovrebbe più essere utilizzata.

5.2.3 SHA-1
Il Secure Hash Algorithm è stato progettato dalla NSA e standardizzato dal NIST [97]. La
prima versione si chiamava solamente SHA (oggi è spesso citata come SHA-0). La NSA
individuò una vulnerabilità nell’algoritmo SHA-0 e sviluppò una correzione pubblicata

76   Capitolo 5

dal NIST come versione migliorata, SHA-1. Tuttavia, non vennero resi noti i dettagli
sulla vulnerabilità.Tre anni più tardi, Chabaud e Joux resero pubblica una vulnerabilità di
SHA-0 [25]. Questa è stata risolta dalla versione migliorata, SHA-1, per cui è ragionevole
pensare che ora si sappia quale fosse il problema.
SHA-1 è una funzione di hash a 160 bit basata su MD4. A causa della parentela comune,
presenta un numero di caratteristiche comuni a MD5, ma ha una struttura molto più
prudente. È anche molto più lenta di MD5. Sfortunatamente, nonostante la progettazione
più prudente, è noto che anche SHA-1 non è sicura.
SHA-1 ha uno stato di 160 bit formato da 5 parole di 32 bit. Come MD5, prevede
quattro cicli sostituiti da una combinazione di operazioni elementari a 32 bit. Anziché
elaborare ogni blocco di messaggio per quattro volte, SHA-1 utilizza una ricorrenza lineare
per “allungare” le parole word di un blocco di messaggio fino alle 80 parole necessarie.
Questa è una generalizzazione della tecnica MD4. In MD5, ciascun bit del messaggio
viene usato quattro volte dalla funzione di mescolamento. In SHA-1, la ricorrenza lineare
assicura che ogni bit del messaggio influisca sulla funzione di mescolamento almeno una
dozzina di volte. È interessante notare come l’unica modifica tra SHA-0 e SHA-1 sia
l’aggiunta di una rotazione di un bit a questa ricorrenza lineare.
Indipendentemente da qualsiasi vulnerabilità interna, il problema principale di SHA-1
è rappresentato dalle dimensioni del risultato, di 160 bit. Le collisioni contro qualsiasi
funzione di hash a 160 bit possono essere generate in soli 280 passaggi, molto al di sotto
del livello di sicurezza dei moderni cifrari a blocchi con dimensioni della chiave da 128
a 256 bit, e insufficiente anche per il livello di sicurezza di 128 bit del nostro progetto.
Anche se il tempo necessario per violare SHA-1 è stato più lungo di quello richiesto
per MD5, ora sappiamo che è possibile trovare collisioni in SHA-1 con un lavoro molto
inferiore ai 280 calcoli di SHA-1 [123]. Ricordate che gli attacchi continuano a migliorare? Non è più sicuro fidarsi di SHA-1.

5.2.4 SHA-224, SHA-256, SHA-384 e SHA-512
Nel 2001 il NIST pubblicò una bozza di standard contenente tre nuove funzioni di
hash, e nel 2004 aggiornò questa specifica per inserire una quarta funzione di hash [101].
Queste funzioni di hash sono conosciute collettivamente come la famiglia di funzioni
di hash SHA-2. Hanno output di 224, 256, 384, e 512 bit, rispettivamente. Sono concepite per essere usate sia con dimensioni della chiave di 128, 192, e 256 bit AES, sia con
la dimensione della chiave di Triple DES, di 112 bit. La loro struttura è molto simile a
quella di SHA-1.
Queste funzioni di hash sono nuove, e la novità in genere è un segnale di pericolo.
Tuttavia, le vulnerabilità note di SHA-1 sono molto più gravi. Inoltre, se si desidera più
sicurezza di quella che SHA-1 è in grado di offrire, è necessaria una funzione di hash con
un risultato più grande. Nessuno dei progetti pubblicati per funzioni di hash più ampie
è stato sottoposto ad approfondita analisi pubblica; la famiglia SHA-2 è stata esaminata
dalla NSA, che in genere sembra sapere ciò che fa.
SHA-256 è molto più lenta di SHA-1. Per messaggi lunghi, calcolare un hash con
SHA-256 richiede quasi il tempo impiegato per cifrare un messaggio con AES o Twofish, o forse più. Questo non è necessariamente un male, ed è una conseguenza del suo
progetto prudente.

Le funzioni di hash   77

5.3 Vulnerabilità delle funzioni di hash
Sfortunatamente, tutte queste funzioni di hash hanno alcune proprietà che le dequalificano rispetto alla nostra definizione di sicurezza.

5.3.1 Estensione della lunghezza
La nostra maggiore preoccupazione riguardo tutte queste funzioni di hash è che presentano un bug di estensione della lunghezza (length-extension) che conduce a problemi
reali e che si sarebbe potuto evitare facilmente. Ecco il problema. Un messaggio m viene
suddiviso in blocchi m1, ... , mk ed elaborato ottenendo un hash di valore H. Ora scegliamo
un messaggio m' che si divide nel blocco m1, ..., mk, mk+1. Poiché i primi k blocchi di m'
sono identici ai k blocchi del messaggio m, il valore di hash h(m) non è altro che il valore
di hash intermedio dopo k blocchi nel calcolo di h(m'). Otteniamo h(m') = h'(h(m), mk+1).
Se si usa MD5 o qualsiasi funzione di hash della famiglia SHA, si deve scegliere m’ con
attenzione in modo che comprenda riempimento e campo della lunghezza, ma questo
non è un problema, perché il metodo per costruire questi campi è noto.
Il problema dell’estensione della lunghezza esiste perché non c’è alcuna elaborazione
speciale al termine del calcolo della funzione di hash. Il risultato è che h(m) fornisce
un’informazione diretta sullo stato intermedio dopo i primi k blocchi di m'.
Questa è di certo una proprietà sorprendente per una funzione a cui vogliamo pensare
come a una mappatura casuale. Infatti, questa proprietà dequalifica immediatamente
tutte le funzioni di hash rispetto alla nostra definizione di sicurezza. Tutto ciò che un
discriminatore deve fare è costruire alcune coppie (m, m') adatte e controllare la validità
di questa relazione. Sicuramente questa relazione non potrebbe esser trovata in una
funzione di hash ideale. Questo è un attacco non generico che sfrutta proprietà delle
stesse funzioni di hash, pertanto è un attacco valido. Richiede solo pochi calcoli di hash,
pertanto è molto veloce.
Quale potrebbe essere il danno causato da questa proprietà? Immaginate un sistema
dove Alice invia un messaggio a Bob e desidera autenticarlo inviando h(X m), dove X
è un segreto noto solo a Bob e Alice, m è il messaggio. Se h fosse una funzione di hash
ideale, questo sarebbe un sistema di autenticazione rispettabile. Ma con l’estensione della
lunghezza, Eve ora è in grado di aggiungere testo al messaggio m e aggiornare il codice
di autenticazione in modo che corrisponda al nuovo messaggio. Un sistema di autenticazione che consenta a Eve di modificare il messaggio è, ovviamente, inutile.
Questo problema verrà risolto in SHA-3; una delle richieste del NIST è che SHA-3
non abbia proprietà di estensione della lunghezza.

5.3.2 Collisione dei messaggi parziali
Un secondo problema è insito nella struttura iterativa della maggior parte delle funzioni
di hash. Lo spieghiamo utilizzando un discriminatore specifico.
Il primo passo di ogni discriminatore è quello di specificare lo scenario nel quale distinguerà tra la funzione di hash e la funzione di hash ideale. A volte questo scenario
può essere molto semplice: data la funzione di hash, trovare una collisione. Qui usiamo
uno scenario leggermente più complicato. Supponete di avere un sistema che autentica
un messaggio m con h(m || X), dove X è la chiave di autenticazione. L’attaccante può
Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

78   Capitolo 5

scegliere il messaggio m, ma il sistema autenticherà un solo messaggio (la maggior parte
dei sistemi consentirà che venga autenticato solo un numero limitato di messaggi; questo
è solo un caso estremo. Nella realtà, molti sistemi inseriscono un numero identificativo
in ciascun messaggio, e per questo attacco ciò ha lo stesso effetto del consentire la scelta
di un solo messaggio).
Per una funzione di hash perfetta di dimensione n, ci si aspetta che questo costrutto
abbia un livello di sicurezza di n bit. L’attaccante non può fare di meglio che scegliere
un m, fare in modo che il sistema lo autentichi come h(m || X) e poi cercare X con una
ricerca esaustiva. Nel caso di una funzione di hash iterativa, invece, l’attaccante può
fare molto di più. Trova due stringhe m e m' che portano a una collisione quando ne
venga calcolato l’hash con h. Questo è fattibile con l’attacco del compleanno in soli
2n/2 passaggi circa. L’attaccante poi fa in modo che il sistema autentichi m e sostituisce
il messaggio con m'. Ricordate che h viene calcolato iterativamente, per cui quando si
verifica una collisione e i rimanenti input dell’hash sono uguali, anche il valore dell’hash
rimane invariato. Dato che calcolare l’hash di m e m' porta allo stesso valore, h(m || X) =
h(m' || X). Notate che questo attacco non dipende da X: gli stessi m e m' funzionerebbero
per tutti i valori di X.
Questo è un tipico esempio di discriminatore, che stabilisce il proprio “gioco” (uno
scenario nel quale tenta un attacco) e quindi attacca il sistema. L’obiettivo è ancora distinguere tra una funzione di hash e la funzione di hash ideale, ma il compito in questo
caso è facile. Se l’attacco riesce, si tratta di una funzione di hash iterativa; se l’attacco
fallisce, è la funzione di hash ideale.

5.4 Rimediare ai punti deboli
Vogliamo una funzione di hash che possa essere trattata come una mappatura casuale, ma
tutte le funzioni di hash conosciute non rispettano questa proprietà. Dobbiamo ricercare eventuali problemi di estensione della lunghezza in ogni punto in cui usiamo una
funzione di hash? Dobbiamo controllare ovunque che non vi siano eventuali collisioni
dei messaggi parziali? Esistono altri punti deboli che dovremmo controllare?
Lasciare delle vulnerabilità nella funzione di hash è una pessima idea. Possiamo garantire
che la funzione prima o poi verrà utilizzata in un modo che ne rivelerà la vulnerabilità.
Perfino se si documentano le vulnerabilità note, nei sistemi reali non verranno verificate.
Anche se si potesse controllare il processo di progettazione così bene, si incorrerebbe in
un problema di complessità. Supponete che la funzione di hash abbia tre vulnerabilità,
il cifrario a blocchi due, lo schema di firma quattro e così via. Prima di accorgervene,
dovrete controllare centinaia di interazioni tra queste vulnerabilità: praticamente impossibile. È necessario correggere la funzione di hash.
Il nuovo standard SHA-3 si occuperà di queste vulnerabilità. Nel frattempo ci servono
dei rimedi a breve termine.

5.4.1 Verso un rimedio a breve termine
Ecco una soluzione potenziale. In realtà consiglieremo i rimedi nei paragrafi seguenti,
mentre questa particolare proposta non è stata oggetto di un’analisi significativa all’interno
della comunità. Ma questa discussione ha scopo illustrativo, perciò la inseriamo qui.

Le funzioni di hash   79

Sia h una delle funzioni di hash menzionate in precedenza. Invece di m |→ h(m), si
potrebbe usare m |→ h(h(m) || m) come funzione di hash (la notazione x |→ f(x) è un
modo di scrivere una funzione senza doverle assegnare un nome. Per esempio, x |→ x2
è una funzione che eleva al quadrato il proprio input). In effetti inseriamo h(m) prima
del messaggio del quale stiamo calcolando l’hash. Questo assicura che i calcoli di hash
dipendano immediatamente da tutti i bit del messaggio, e nessun attacco a messaggio
parziale o a estensione della lunghezza potrà funzionare.
Definizione 6
Se h e una funzione di hash iterativa. La funzione di hash hDBL viene definita da
hDBL(m) := h(h(m || m).

Riteniamo che, se h è una qualsiasi delle nuove funzioni di hash della famiglia SHA-2,
questo costrutto avrà un livello di sicurezza di n bit, dove n è la dimensione del risultato
dell’hash.
Uno svantaggio di questo approccio è la sua lentezza. È necessario calcolare l’hash dell’intero messaggio per due volte, il che richiede il doppio del tempo. Un altro svantaggio è
che questo approccio richiede che l’intero messaggio m venga memorizzato in un buffer.
Non si può più calcolare l’hash di un flusso di dati mentre si presenta.Alcune applicazioni
si basano su questa abilità, e con esse non sarebbe possibile usare hDBL.

5.4.2 Un rimedio a breve termine più efficiente
Come possiamo mantenere la piena velocità della funzione di hash originale? Con un
piccolo trucco. Invece di h(m) possiamo usare h(h(0b || m)) come funzione di hash e affermare di avere un livello di sicurezza di soli n/2 bit. Qui b rappresenta la lunghezza di
blocco della funzione di compressione sottostante, per cui 0b || m equivale ad anteporre
al messaggio un blocco di tutti zeri prima di calcolare l’hash. Il trucco sta nel fatto che
normalmente ci aspettiamo che una funzione di hash di n bit fornisca un livello di sicurezza di n bit per le situazioni in cui un attacco basato sulle collisioni non è possibile
(anche la documentazione di SHA-256 sostiene che una funzione di hash a n bit dovrebbe
richiedere 2n passaggi per trovare una preimmagine di un dato valore). Gli attacchi basati
sulle collisioni dei messaggi parziali si basano tutti sull’attacco del compleanno, perciò
se riduciamo il livello di sicurezza a n/2 bit questi attacchi non ricadono più nel livello
di sicurezza dichiarato.
Nella maggior parte delle situazioni ridurre il livello di sicurezza in questo modo non
sarebbe accettabile, ma qui siamo fortunati. Le funzioni di hash sono già progettate per
essere utilizzate in situazioni nelle quali gli attacchi di collisione sono possibili, perciò le
dimensioni delle funzioni di hash sono sufficientemente grandi. Se applichiamo questo
costrutto a SHA-256, otteniamo una funzione di hash con un livello di sicurezza di 128
bit, che è esattamente quello che ci serve.
Qualcuno potrebbe sostenere che tutte le funzioni di hash a n bit forniscono solo n/2 bit
di sicurezza. È un punto di vista valido. Sfortunatamente, a meno che siate molto precisi
su queste cose, le persone abuseranno della funzione di hash e daranno per scontato che
fornisca n bit di sicurezza. Per esempio, qualcuno vorrà utilizzare SHA-256 per generare
una chiave di 256 bit per AES, presupponendo che fornirà un livello di sicurezza di 256
bit. Come abbiamo spiegato in precedenza, usiamo chiavi di 256 bit per fornire un livello

80   Capitolo 5

di sicurezza di 128 bit, quindi ciò si combina perfettamente con il livello di sicurezza
ridotto della nostra versione fissa di SHA-256. Non è un fatto casuale: in entrambi i casi,
il divario tra la dimensione del valore crittografico e il livello di sicurezza richiesto è
dovuto agli attacchi di collisione. Poiché supponiamo che gli attacchi di collisione siano
sempre possibili, le dimensioni e i livelli di sicurezza diversi si combineranno bene.
Riportiamo una definizione più formale del rimedio qui presentato.
Definizione 7
Sia h una funzione di hash iterativa, e sia b l’indicazione della lunghezza di blocco della
funzione di compressione sottostante. La funzione di hash hd è definita da hd (m):= h(h(0b
|| m)), e ha un livello di sicurezza richiesto pari a min(k, n/2) dove k è il livello di sicurezza
di h e n rappresenta la dimensione del risultato dell’hash.

Useremo questo costrutto principalmente in combinazione con le funzioni di hash della
famiglia SHA. Per ogni funzione hash SHA-X, dove X è1, 224, 256, 384 o 512, definiamo
SHAd-X come la funzione che trasforma m in SHA-X(SHA-X(0b || m)). Per esempio,
SHAd-256 è semplicemente la funzione m |→ SHA-256(SHA-256(0512 || m)).
Questo particolare rimedio per la famiglia SHA di funzioni di hash iterative, oltre a essere
collegato al costrutto del Paragrafo 5.4.1, è stato anche descritto da Coron et al. [26].
Si può dimostrare che la funzione di hash fissa hd è forte almeno quanto la sottostante
funzione di hash h (in realtà qui c’è un piccolo trucco. Effettuando un doppio hash,
l’intervallo della funzione viene ridotto, e gli attacchi del compleanno risultano un po’
più facili. È un piccolo effetto, e rientra ampiamente nel margine di approssimazione cha
abbiamo utilizzato altrove). HMAC usa un simile approccio del tipo “calcola un altro
hash” per proteggere dagli attacchi a estensione della lunghezza. Anteponendo al messaggio un blocco di zeri, fa in modo che, a meno del verificarsi di qualcosa di insolito, il
primo blocco in input verso la funzione di hash interna hd sia diverso dall’input verso la
funzione di hash esterna. Sia hDBL che hd eliminano il bug dell’estensione della lunghezza
che costituisce il problema principale per i sistemi reali. Se hDBL abbia in effetti un livello
di sicurezza di n bit resta ancora da vedere. Potremmo fidarci di entrambi fino a n/2 bit
di sicurezza, per cui nella pratica utilizzeremmo il più efficiente costrutto hd.

5.4.3 Un altro rimedio
Esiste un altro rimedio per alcune delle vulnerabilità della famiglia di funzioni di hash
iterative SHA-2: troncare l’output [26]. Se una funzione di hash produce output di n bit,
basta usare solo i primi n − s di questi bit come valore di hash per qualche s positivo. In
effetti, SHA-224 a e SHA-384 lo fanno già; SHA-224 corrisponde pressappoco a SHA256 con 32 bit di output esclusi, e SHA-384 corrisponde più o meno a SHA-512 con
il troncamento di 128 bit di output. Per una sicurezza di 128 bit, si potrebbe calcolare
l’hash con SHA-512, omettere 256 bit dell’output e restituire i rimanenti 256 bit come
risultato della funzione di hash troncata. Il risultato sarebbe una funzione di hash a 256
bit che, a causa degli attacchi del compleanno, soddisferebbe la richiesta di progetto di
un livello di sicurezza di 128 bit.

Le funzioni di hash   81

5.5 Quale funzione di hash scegliere?
Molte delle proposte presentate al concorso per SHA-3 del NIST esibiscono nuove e
rivoluzionarie concezioni e affrontano le vulnerabilità trattate qui e altri problemi. Il
concorso, però, si sta ancora svolgendo, e il NIST non ha ancora scelto un algoritmo
SHA-3 definitivo. Per avere sufficiente confidenza con le proposte SHA-3 è necessario
un ulteriore lavoro di analisi. Nel breve termine raccomandiamo di utilizzare una delle
più recenti funzioni di hash della famiglia SHA: SHA-224, SHA-256, SHA-384 o SHA512. Inoltre, suggeriamo di scegliere una funzione di hash della famiglia SHAd, o di
usare SHA-512 e troncare l’output a 256 bit. Nel lungo termine, molto probabilmente
raccomanderemo il vincitore del concorso SHA-3.

5.6 Esercizi
Esercizio 5.1 Usate uno strumento software per generare due messaggi M e M’, M
≠ M’, che producano una collisione per MD5. Per generare questa collisione, usate uno
degli attacchi conosciuti a MD5. Un collegamento a un esempio di codice per l’individuazione di collisioni MD5 è disponibile presso http://www.schneier.com/ce.html.
Esercizio 5.2 Usando una libreria crittografica esistente, scrivete un programma per
calcolare il valore di hash SHA-512 del seguente messaggio in esadecimale:
48 65 6C 6C 6F 2C 20 77 6F 72 6C 64 2E 20 20 20

Esercizio 5.3 Considerate SHA-512-n, una funzione di hash che dapprima esegue
SHA-512 e quindi invia in output solo i primi n bit del risultato. Scrivete un programma
che usi un attacco del compleanno per individuare e inviare in output una collisione
su SHA-512-n, dove n è un multiplo di 8 compreso tra 8 e 48. Il programma può utilizzare una libreria crittografica esistente. Calcolate il tempo impiegato dal programma
quando n è 8, 16, 24, 32, 40 e 48, su una media di cinque cicli per ogni n. Quanto tempo
vi aspettate che il programma impiegherà per SHA-512-256? Per SHA-512-384? Per
SHA-512?
Esercizio 5.4 Sia SHA-512-n come nel precedente esercizio. Scrivete un programma che trovi un messaggio M (una preimmagine) il cui hash sotto SHA-512-8 abbia il
seguente valore (in esadecimale):
A9

Scrivete un programma che trovi un messaggio M il cui hash sotto SHA-512-16 abbia
il seguente valore (in esadecimale):
3D 4B

Scrivete un programma che trovi un messaggio M il cui hash sotto SHA-512-24 abbia
il seguente valore (in esadecimale):
3A 7F 27

82   Capitolo 5

Scrivete un programma che trovi un messaggio M il cui hash sotto SHA-512-32 abbia
il seguente valore (in esadecimale):
C3 C0 35 7C

Calcolate il tempo impiegato dal programma quando n è 8, 16, 24 e 32, su una media di
cinque cicli ciascuno. I programmi possono utilizzare una libreria crittografica esistente.
Quanto tempo vi aspettate che impiegherà un simile programma per SHA-512-256?
Per SHA-512-384? Per SHA-512?
Esercizio 5.5 Nel Paragrafo 5.2.1 abbiamo sostenuto che m e m’ hanno lo stesso hash
di H2. Mostrate perché questa affermazione è vera.
Esercizio 5.6 Scegliete due delle proposte di funzione di hash candidate per SHA-3
e confrontate le loro prestazioni e la loro sicurezza rispetto agli attacchi più noti. Informazioni sulle funzioni candidate per SHA-3 sono disponibili presso http://www.schneier.
com/ce.html.

Capitolo 6

I codici
di autenticazione
dei messaggi
Un codice di autenticazione del messaggio o MAC (Message Authentication Code) è un costrutto che individua
le manomissioni dei messaggi. La cifratura impedisce
a Eve di leggere i messaggi, ma non le impedisce di
manipolarli. È qui che entra in gioco il MAC. Come
la cifratura, anche i MAC utilizzano una chiave segreta, K, nota sia ad Alice che a Bob ma non a Eve.
Alice non invia solo il messaggio m, ma anche un
valore MAC calcolato da una funzione MAC. Bob
verifica che il valore di MAC del messaggio ricevuto
corrisponda al valore MAC ricevuto: se non vi è corrispondenza, scarta il messaggio considerandolo non
autenticato. Eve non è in grado di manipolare il messaggio, perché senza K non può trovare il giusto valore
MAC da inviare con il messaggio manipolato.
In questo capitolo trattiamo soltanto l’autenticazione,
mentre il meccanismo per combinare cifratura e autenticazione verrà preso in esame nel Capitolo 7.

6.1 Azione di un MAC
Un MAC è una funzione che accetta due argomenti,
una chiave a dimensione fissa K e un messaggio di
dimensioni arbitrarie m, e genera una valore di MAC
di dimensione fissa. Scriveremo la funzione MAC
come MAC(K, m). Per autenticare un messaggio,
Alice non invia solo il messaggio m ma anche il codice
MAC MAC(K, m), chiamato anche tag. Supponete
che Bob, anche lui in possesso della chiave K, riceva
un messaggio m' e un tag T. Bob utilizza l’algoritmo
di verifica del MAC per assicurarsi che T sia un MAC
valido sotto la chiave K per il messaggio m'.

Sommario
6.1 Azione di un MAC
6.2 Il MAC ideale e la
sicurezza del MAC
6.3 CBC-MAC e CMAC
6.4 HMAC
6.5 GMAC
6.6 Quale MAC scegliere?
6.7 Utilizzo di un MAC
6.8 Esercizi

84   Capitolo 6

Cominciamo a esaminare la funzione MAC in isolamento. Tenete presente che utilizzare
una funzione MAC nella maniera corretta è più complicato che limitarsi ad applicarla
al messaggio. Affronteremo questi problemi più avanti, nel Paragrafo 6.7.

6.2 Il MAC ideale e la sicurezza del MAC
Esistono diversi modi per definire la sicurezza di un MAC. Qui descriveremo la nostra
definizione preferita, che si basa sul concetto di una funzione MAC ideale, molto simile
alla nozione di un cifrario a blocchi ideale. La differenza principale è che i cifrari a blocchi
sono permutazioni, cosa che i MAC non sono. Questa è la nostra definizione preferita
perché tiene conto di un gran numero di attacchi, compresi quelli a chiave debole, quelli
a chiavi correlate e altri ancora.
Il MAC ideale è una mappatura casuale. Sia n il numero di bit nel risultato di un MAC;
la nostra definizione di MAC ideale è la seguente.
Definizione 8
Una funzione MAC ideale è una mappatura casuale da tutti gli input possibili su output
di n bit.

Ricordate che in questa definizione il MAC accetta due input: una chiave e un messaggio. Nella pratica la chiave K non è nota all’attaccante o, più precisamente, non è
completamente nota. Potrebbe esservi una vulnerabilità nel resto del sistema che lascia
filtrare all’attaccante informazioni parziali su K.
Definiamo la sicurezza di un MAC nel modo seguente.
Definizione 9
Un attacco contro un MAC è un metodo non generico per distinguere il MAC da una
funzione MAC ideale.

La crittografia è un campo vasto e comprende definizioni più formali utilizzate dai
teorici. Quando possibile, tuttavia, preferiamo la definizione qui fornita perché è più
generica e più allineata con l’intero insieme di attacchi che si potrebbero considerare. Il
nostro modello degli attacchi comprende alcune forme non intercettate dalle definizioni
formali convenzionali, come gli attacchi a chiavi correlate e quelli che presuppongono
che l’attaccante abbia una conoscenza parziale della chiave. Questo è il motivo per cui
preferiamo le nostre definizioni, che rimangono robuste anche quando la funzione viene
abusata o utilizzata in un ambiente insolito.
La più restrittiva definizione standard è quella in cui un attaccante sceglie n messaggi
diversi a piacimento e riceve il valore MAC per ognuno di essi. L’attaccante deve quindi
ricavare n + 1 messaggi, ciascuno con un valore MAC valido.

6.3 CBC-MAC e CMAC
CBC-MAC è un metodo classico per trasformare un cifrario a blocchi in un MAC. La
chiave K viene usata come chiave del cifrario a blocchi. L’idea alla base di CBC-MAC è

I codici di autenticazione dei messaggi    85

di cifrare il messaggio m usando la modalità CBC e quindi scartare tutto tranne l’ultimo
blocco di testo cifrato. Per un messaggio P1, ..., Pk, il MAC viene calcolato come:
H0 := IV
Hi := EK (Pi ⊕ Hi−1)
MAC := Hk
A volte per l’output della funzione CBC-MAC si considera solo una parte (per esempio
la metà) dell’ultimo blocco. La più comune definizione di CBC-MAC richiede che l’IV
sia fissato a 0.
In generale non si dovrebbe mai usare la stessa chiave per la cifratura e per l’autenticazione.
È particolarmente pericoloso usare la cifratura CBC e l’autenticazione CBC-MAC con
la stessa chiave. Il MAC finisce per essere uguale all’ultimo blocco di testo cifrato. Inoltre,
in base a quando e come vengono applicate la cifratura CBC e CBC-MAC, usare la
medesima chiave in entrambi i casi può portare a compromissioni della riservatezza per
la cifratura CBC e compromissioni dell’autenticazione per CBC-MAC.
CBC-MAC è un po’ complicato da usare, ma viene generalmente considerato sicuro se
utilizzato correttamente e quando il cifrario sottostante è sicuro. Studiare i punti di forza
e le vulnerabilità di CBC-MAC può essere molto istruttivo. Ci sono diversi attacchi di
collisione su CBC-MAC che in effetti limitano la sicurezza alla metà della lunghezza della
dimensione del blocco [20]. Ecco un semplice attacco di collisione: sia M una funzione
CBC-MAC. Se sappiamo che M(a) = M(b), allora sappiamo anche che M(a || c) =
M(b || c). Questo è dovuto alla struttura di CBC-MAC. Spieghiamolo con un semplice
caso: c è formato da un singolo blocco. Abbiamo:
M(a || c) = EK(c ⊕ M(a))
M(b || c) = EK(c ⊕ M(b))
E questi due devono essere uguali, perché M(a) = M(b).
L’attacco prosegue in due fasi. Nella prima fase, l’attaccante raccoglie i valori di MAC
di un gran numero di messaggi finché si verifica una collisione. Questo richiede 264
passaggi per un cifrario a blocchi a 128 bit, a causa del paradosso del compleanno. Si
ottengono così l’a e il b per i quali M(a) = M(b). Se ora l’attaccante può fare in modo
che il mittente autentichi a || c, allora potrà sostituire il messaggio con b || c senza cambiare il valore MAC.
Il ricevente verificherà il MAC e accetterà il messaggio contraffatto b || c (ricordate che
stiamo lavorando con il “modello paranoia”. È del tutto accettabile che l’attaccante
crei un messaggio e lo faccia autenticare dal mittente. Esistono molte situazioni in cui
ciò è possibile). Esistono molte estensioni di questo attacco che funzionano anche con
l’aggiunta di campi di lunghezza e regole di riempimento [20].
Questo non è un attacco generico, dato che non funziona su una funzione MAC ideale.
Trovare la collisione non è un problema; lo si può fare per una funzione MAC ideale
esattamente nello stesso modo. Ma una volta ottenuti due messaggi a e b per i quali
M(a) = M(b), non è possibile usarli per creare un falso MAC su un nuovo messaggio,
mentre questo è possibile con CBC-MAC.
Come altro esempio di attacco, supponete che c sia lungo un blocco, e M(a M(b || c)
= EK(c ⊕ M(b)) c) = M(b || c). Allora M(a || d) = M(b || d) per qualsiasi blocco d. Questo
attacco è simile a quello visto in precedenza. Per prima cosa l’attaccante raccoglie i valori

86   Capitolo 6

MAC di un gran numero di messaggi che finiscono in c finché si verifica una collisione.
Questo fornisce i valori di a e b. Poi l’attaccante fa sì che il mittente autentichi a || d. Ora
è in grado di sostituire il messaggio con b || d senza cambiare il valore MAC.
Esistono interessanti risultati teorici che sostengono che, nel particolare modello di prova
utilizzato, CBC-MAC assicura 64 bit di sicurezza quando la dimensione del blocco è di
128 bit [6] e quando il MAC viene applicato solamente ai messaggi che hanno la stessa
dimensione. Sfortunatamente questo livello di sicurezza è inferiore a quello richiesto per
il nostro progetto, anche se in pratica non è subito chiaro come sia possibile raggiungerlo
con cifrari a blocchi di 128 bit. CBC-MAC andrebbe bene se si potesse utilizzare un
cifrario a blocchi con una dimensione del blocco di 256 bit.
Esistono altri motivi per cui è necessario prestare attenzione al modo in cui CBC-MAC
viene utilizzato. Non si può semplicemente applicare CBC-MAC al messaggio, se si
vogliono autenticare messaggi di lunghezza diversa, dato che questo porterebbe alla
possibilità di attacchi semplici. Per esempio, supponete che a e b siano entrambi lunghi
un blocco, e che i MAC del mittente siano a, b e a || b. Un attaccante che intercetta i
tag di MAC per questi messaggi può generare un falso MAC per il messaggio b || (M(b)
⊕ M(a) ⊕ b), che non è mai stato inviato dal mittente. Il falso tag creato per questo
messaggio è uguale a M(a || b), il tag per a || b. Come esercizio potete provare a capire il
motivo per cui vale ciò, ma il problema deriva dal fatto che il mittente utilizza il MAC
su messaggi di dimensioni diverse.
Se volete usare CBC-MAC, dovete invece procedere come segue.
1. Costruire una stringa s dalla concatenazione di l e m, dove l è la lunghezza di m
codificata in un formato a lunghezza fissa.
2. Riempire s finché la lunghezza sia un multiplo della dimensione di blocco (maggiori
dettagli nel Paragrafo 4.1).
3. Applicare CBC-MAC alla stringa s ottenuta.
4. Inviare in output l’ultimo blocco di testo cifrato, o parte di esso. Non inviare in
output alcuno dei valori intermedi.
Il vantaggio di CBC-MAC sta nel fatto di utilizzare lo stesso tipo di calcoli delle modalità di
cifratura dei cifrari a blocchi. In molti sistemi la cifratura e il MAC sono le due sole funzioni
che vengono applicate ai dati grezzi, per cui rappresentano le aree in cui la velocità è un
fattore critico. Facendo in modo che utilizzino le stesse funzioni primitive si semplifica la
realizzazione di implementazioni efficienti, in particolare quando si tratta di hardware.
Nonostante tutto, non sosteniamo l’utilizzo diretto di CBC-MAC, perché è difficile da
utilizzare nel modo corretto.
Un’alternativa che ci sentiamo di raccomandare è CMAC [42]. CMAC si basa su CBCMAC ed è stato standardizzato di recente dal NIST. Funziona quasi esattamente come
CBC-MAC, tranne per la diversa gestione dell’ultimo blocco. Nello specifico, CMAC
esegue lo xor di uno di due valori speciali nell’ultimo blocco prima dell’ultima cifratura
del cifrario a blocchi. Questi valori speciali vengono ricavati dalla chiave di CMAC, e
quello utilizzato da CMAC dipende dal fatto che la lunghezza del messaggio sia o meno
un multiplo della lunghezza del blocco del cifrario a blocchi. Lo xor di questi valori
nel MAC scompagina gli attacchi che compromettono CBC-MAC quando viene usato
per messaggi di lunghezza diversa.

I codici di autenticazione dei messaggi    87

6.4 HMAC
Posto che il MAC ideale è una mappatura casuale con chiavi e messaggi come input,
e che disponiamo già delle funzioni di hash che si comportano (o meglio cercano di
comportarsi) come mappature casuali con i messaggi in input, è un’idea ovvia utilizzare
una funzione di hash per costruire un MAC. Questo è esattamente ciò che fa HMAC
[5, 81]. I progettisti di HMAC erano ovviamente consapevoli dei problemi delle funzioni di hash, esaminati nel Capitolo 5. Per questo motivo non hanno definito HMAC
in modo che fosse qualcosa di semplice come MAC(K, m), come h(K || m), h(m || K) o
perfino come h(K || m || K), che possono creare problemi se si utilizza una delle funzioni
di hash iterative standard [103].
Invece, HMAC calcola h(K ⊕ a || h(K ⊕ b || m)), dove a e b sono costanti specificate. Lo
stesso messaggio subisce un solo hash, e l’output viene nuovamente sottoposto a hash
con la chiave. Per i dettagli fate riferimento alle specifiche in [5, 81]. HMAC funziona
con qualsiasi funzione di hash iterativa tra quelle citate nel Capitolo 5. Inoltre, a causa
della sua progettazione, non è soggetto agli stessi attacchi di collisione che hanno recentemente minato la sicurezza di SHA-1 [4]. Questo perché, nel caso di HMAC, l’inizio del
messaggio da sottoporre a hash si basa su una chiave segreta e non è noto all’attaccante.
Ciò significa che HMAC con SHA-1 non è una cattiva scelta come SHA-1 da solo.
Tuttavia, dato che gli attacchi spesso migliorano col tempo, ora riteniamo HMAC con
SHA-1 una scelta troppo rischiosa e non ne raccomandiamo l’impiego.
I progettisti realizzarono HMAC con attenzione perché fosse in grado di resistere agli
attacchi, e verificarono i limiti di sicurezza sul costrutto ottenuto. HMAC evita gli attacchi
di recupero della chiave che rivelano K all’attaccante, ed evita gli attacchi che possono
essere condotti dall’attaccante senza interazione con il sistema. Tuttavia, HMAC, come
CMAC, è ancora limitato a n/2 bit di sicurezza, dato che esistono attacchi del compleanno
generici contro la funzione che fanno uso delle collisioni interne della funzione di hash
iterata. La struttura di HMAC assicura che questi attacchi richiedano 2n/2 interazioni
con il sistema sotto attacco, cosa più difficile da realizzare rispetto a svolgere 2n/2 calcoli
sul proprio computer.
La documentazione di HMAC [5] presenta molti buoni esempi dei problemi che sorgono
quando le primitive (in questo caso la funzione di hash) hanno proprietà inaspettate.
Questo è il motivo per cui siamo così insistenti riguardo al fornire specifiche di comportamento semplici per le nostre primitive di crittografia.
Ci piace la struttura di HMAC. È pulita, efficiente e facile da implementare. È utilizzata spesso con la funzione di hash SHA-1, e la troverete in molte librerie. Tuttavia, per
raggiungere il nostro livello di sicurezza di 128 bit dovremmo utilizzarla solo con una
funzione di hash a 256 bit come SHA-256.

6.5 GMAC
Il NIST ha di recente standardizzato un nuovo MAC, denominato GMAC [43], molto
efficiente sia dal lato hardware sia da quello software. GMAC è stato progettato per
cifrari a blocchi a 128 bit.
GMAC è radicalmente diverso da CBC-MAC, CMAC e HMAC. La funzione di autenticazione di GMAC prende come input tre valori: la chiave, il messaggio da autenticare

88   Capitolo 6

e un nonce (ricordate che il nonce è un valore che viene utilizzato una e una sola volta).
CBC-MAC, CMAC e HMAC non accettano un nonce come input. Se un utente applica
il MAC a un messaggio con una chiave e un nonce, il nonce dovrà essere conosciuto
anche al destinatario. L’utente potrebbe inviare esplicitamente il nonce al destinatario,
oppure il nonce potrebbe essere implicito, come un contatore di pacchetti gestito sia dal
mittente che dal destinatario.
Data la sua diversa interfaccia, GMAC non rispetta la nostra definizione preferita di
MAC fornita nel Paragrafo 6.2, che implica l’impossibilità di poter essere distinta da una
funzione MAC ideale. Invece, dobbiamo usare la definizione di non falsificabilità menzionata alla fine del paragrafo. In sostanza, consideriamo un modello in cui l’attaccante
sceglie n diversi messaggi a suo piacimento e ottiene il valore MAC per ciascuno di essi.
L’attaccante poi deve ricavare n + 1 messaggi, ciascuno con un valore MAC valido; se
non riesce in questo compito, allora il MAC è non falsificabile.
Dietro le quinte, GMAC utilizza una cosiddetta funzione di hash universale [125]. Questa
è molto diversa dai tipi di funzione di hash esaminati nel Capitolo 5. I dettagli del funzionamento delle funzioni di hash universali vanno al di là dello scopo di questo libro,
ma si può considerare GMAC come il calcolo di una semplice funzione matematica del
messaggio in input. Questa funzione è molto più semplice di SHA-1, SHA-256 o simili.
GMAC cifra l’output della funzione con un cifrario a blocchi in modalità CTR per
ottenere il tag, e utilizza una funzione del suo nonce come IV per la modalità CTR.
GMAC è standardizzato e rappresenta una scelta ragionevole in molte circostanze. Ma
riteniamo utile qualche avvertimento. Come HMAC e CMAC, GMAC garantisce al più
64 bit di sicurezza. Alcune applicazioni potrebbero voler utilizzare tag più corti di 128
bit. A differenza di HMAC e CMAC, GMAC offre una sicurezza indebolita per questi
ridotti valori di tag. Supponete che un’applicazione utilizzi GMAC ma tronchi i tag in
modo che siano lunghi 32 bit. Ci si potrebbe aspettare che il sistema risultante offra 32
bit di sicurezza, ma in effetti è possibile creare un falso MAC dopo 216 tentativi [48].
La nostra raccomandazione è di non usare GMAC quando si devono generare valori
MAC di piccole dimensioni.
Infine, richiedere al sistema di fornire un nonce può essere rischioso, perché la sicurezza
può essere annullata se il sistema fornisce lo stesso valore per il nonce più di una volta.
Come abbiamo visto nel Paragrafo 4.7, i sistemi reali continuano ad avere problemi perché
non gestiscono correttamente la generazione del nonce. Raccomandiamo pertanto di
evitare modalità che espongano valori nonce a sviluppatori di applicazioni.

6.6 Quale MAC scegliere?
Come forse avete capito dalla discussione precedente, la nostra scelta ricadrebbe su
HMAC-SHA-256: la struttura di HMAC che usa SHA-256 come funzione di hash.
Vogliamo davvero utilizzare tutti i 256 bit del risultato. La maggior parte dei sistemi
usa valori MAC di 64 o 96 bit, e anche quello potrebbe sembrare un grande carico di
lavoro. Per quanto ne sappiamo, non esiste un attacco di collisione sul valore MAC se
viene utilizzato nel modo tradizionale, per cui troncare i risultati di HMAC-SHA-256
a 128 bit dovrebbe essere sicuro, stanti le attuali conoscenze nel settore.
Questa situazione non ci rende particolarmente felici, poiché crediamo che dovrebbe
essere possibile creare funzioni MAC più veloci. Ma finché funzioni adatte saranno

I codici di autenticazione dei messaggi    89

pubblicate e analizzate, e diverranno ampiamente accettate, non c’è molto che possiamo
fare al riguardo. GMAC è veloce, ma fornisce al più 64 bit di sicurezza e non è adatto
per essere usato per produrre tag brevi. Richiede anche un nonce, e questa è una fonte
comune di problemi di sicurezza.
Tra le proposte inviate al concorso per SHA-3 del NIST presentano modalità speciali
che consentono di utilizzarle per creare MAC più veloci. Ma il concorso non si è ancora concluso ed è troppo presto per dire con quanta confidenza sarà possibile giudicare
sicure determinate proposte.

6.7 Utilizzo di un MAC
L’utilizzo corretto di un MAC è molto più complicato di quanto non possa sembrare a
prima vista. Esaminiamo nel seguito i problemi principali.
Quando Bob riceve il valore MAC(K, m), sa che qualcuno che conosceva la chiave K
ha approvato il messaggio m. Quando si usa un MAC, si deve prestare molta attenzione al fatto che siano valide tutte le proprietà di sicurezza necessarie. Per esempio, Eve
potrebbe registrare un messaggio inviato da Alice a Bob, e poi inviare una copia a Bob
in un secondo momento. Senza una protezione speciale contro questo tipo di attacchi,
Bob accetterebbe il messaggio di Eve come se fosse un messaggio valido di Alice. Simili problemi si avrebbero se Alice e Bob usassero la stessa chiave K per il traffico nelle
due direzioni. Eve potrebbe rimandare il messaggio ad Alice, la quale crederebbe che
provenga da Bob.
In molte situazioni Alice e Bob vogliono autenticare non solo il messaggio m, ma anche
dei dati supplementari d. Questi dati aggiuntivi comprendono elementi come il numero
di messaggio utilizzato per prevenire gli attacchi a ripetizione, l’origine e la destinazione
del messaggio e così via. Molto spesso questi campi fanno parte dell’intestazione del
messaggio autenticato (e spesso cifrato). Il MAC deve autenticare sia d che m. La soluzione
generale consiste nell’applicare il MAC a d || m invece che solo a m (stiamo assumendo
che la mappatura da d e m a d || m sia uno a uno, altrimenti dovremmo utilizzare una
codifica migliore).
Il punto successivo è ben descritto nella seguente regola di progettazione:
Principio di Horton: si deve autenticare il significato, non le parole con cui è espresso.
Un MAC autentica solo una stringa di byte, mentre Alice e Bob vogliono autenticare
un messaggio con uno specifico significato. Il divario tra quanto viene detto (cioè i byte
inviati) e quanto si voleva dire (cioè l’interpretazione del messaggio) è importante.
Supponete che Alice usi il MAC per autenticare m := a || b || c, dove a, b e c sono alcuni campi di dati. Bob riceve m e lo suddivide in a, b e c. Ma che criterio usa Bob per
suddividere m nei vari campi? Bob deve avere delle regole, e se quelle regole non sono
compatibili con il modo in cui Alice ha costruito il messaggio, Bob otterrà valori errati
per i campi. Sarebbe un fatto negativo, dato che Bob avrebbe ricevuto dati autenticati
ma fasulli. Pertanto, è fondamentale che Bob possa suddividere m nei campi che Alice
aveva inserito.
Raggiungere questo scopo è facile nei sistemi semplici, in cui i campi hanno una dimensione fissa. Ma presto vi troverete in situazioni in cui alcuni campi devono avere
dimensioni variabili, oppure una nuova versione del software potrà utilizzare campi più

90   Capitolo 6

grandi. Ovviamente, una versione nuova dovrà essere compatibile con il vecchio software
per poter comunicare con esso. E qui sta il problema. Una volta che la lunghezza dei
campi non è più costante, Bob la dovrà ricavare da qualche contesto, e quel contesto
potrebbe essere manipolato dall’attaccante. Per esempio, Alice utilizza il vecchio software e i vecchi campi di piccole dimensioni. Bob usa il nuovo software. Eve, l’attaccante,
manipola le comunicazioni tra Alice e Bob per far credere a Bob che si stia utilizzando il
nuovo protocollo (i dettagli su come questo sia possibile non sono importanti; il sistema
del MAC non dovrebbe basarsi sul presupposto che altre parti del sistema siano sicure).
Bob suddivide tranquillamente il messaggio usando le dimensioni di campo maggiori,
e ottiene dati fasulli.
È qui che entra in gioco il principio di Horton [122] (il nome deriva da uno dei personaggi di Dr. Seuss, un autore di libri per ragazzi molto noto negli Stati Uniti [116]).
Si deve autenticare il significato, non le parole con cui è espresso. Ciò significa che il
MAC dovrebbe autenticare non solo m, ma anche tutte le informazioni che Bob usa
per estrarre da m il suo significato. Così verrebbero compresi dati come l’identificatore
del protocollo, il numero di versione del protocollo, l’identificatore di messaggio del
protocollo, le dimensioni dei diversi campi e così via. Una soluzione parziale è quella
di non limitarsi a concatenare i dati, ma usare una struttura di dati come XML che può
essere analizzata senza ulteriori informazioni.
Il principio di Horton costituisce uno dei motivi per cui l’autenticazione a livelli di
protocollo inferiori non è adeguata per i protocolli di livello superiore. Un sistema di
autenticazione a livello dei pacchetti IP non può conoscere il modo in cui il programma
di email interpreterà i dati. Ciò impedisce di verificare che il contesto entro il quale il
messaggio viene interpretato sia identico a quello in cui è stato inviato. L’unica soluzione
è quella di fare in modo che il programma di email fornisca la propria autenticazione
dei dati scambiati, ovviamente oltre all’autenticazione sui livelli inferiori.
Per riassumere: quando eseguite un’autenticazione, pensate sempre attentamente a quali
altre informazioni dovrebbero essere comprese in essa. Assicuratevi di codificare tutte
queste informazioni, compreso il messaggio, in una stringa di byte in modo che possa
essere nuovamente convertita nei campi in maniera univoca. Non dimenticate di applicare questo principio alla concatenazione dei dati aggiuntivi e del messaggio di cui
si è discusso all’inizio di questo paragrafo. Se autenticate d M(b || c) = EK(c ⊕ M(b)) m,
dovreste avere una regola prestabilita per come suddividere il risultato della concatenazione nei componenti d e m.

6.8 Esercizi
Esercizio 6.1 Descrivete un sistema realistico che usi CBC-MAC per l’autenticazione
del messaggio e sia vulnerabile a un attacco a estensione di lunghezza contro CBCMAC.
Esercizio 6.2 Supponete che c sia lungo un blocco, a e b siano stringhe di un multiplo
della lunghezza del blocco, e M(a || c) = M(b || c). Qui M è CBC-MAC. Quindi
M(a || d) = M( || d) per qualsiasi blocco d. Spiegate perché questa affermazione è vera.

I codici di autenticazione dei messaggi    91

Esercizio 6.3 Supponete che a e b siano entrambi lunghi un blocco, e supponete
che il mittente ricavi il MAC di a, b e a M(b || c) = EK(c ⊕ M(b)) b con CBC-MAC. Un
attaccante che intercetti i tag di MAC per questi messaggi ora è in grado di falsificare
il MAC per il messaggio b || (M(b) ⊕ M(a) ⊕ b), che il mittente non ha mai inviato.
Il tag falsificato per questo messaggio è uguale a M(a || b), il tag per a || b. Giustificate
matematicamente perché ciò e vero.
Esercizio 6.4 Supponete che il messaggio a sia lungo un blocco. Supponete che un
attaccante abbia ricevuto il MAC t per a usando CBC-MAC con qualche chiave casuale
di cui non ha conoscenza. Spiegate come falsificare il MAC per un messaggio di due
blocchi di vostra scelta. Qual è il messaggio di due blocchi che avete scelto? Qual è il
tag che avete scelto? Perché il tag che avete scelto è valido per il vostro messaggio di
due blocchi?
Esercizio 6.5 Usando una libreria crittografica esistente, calcolate il MAC del messaggio:
4D 41 43 73 20 61 72 65 20 76 65 72 79 20 75 73
65 66 75 6C 20 69 6E 20 63 72 79 70 74 6F 67 72
61 70 68 79 21 20 20 20 20 20 20 20 20 20 20 20

utilizzando il CBC-MAC con AES e la chiave di 256 bit:
80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01

Esercizio 6.6 Usando una libreria crittografica esistente calcolate il MAC del messaggio:
4D 41 43 73 20 61 72 65 20 76 65 72 79 20 75 73
65 66 75 6C 20 69 6E 20 63 72 79 70 74 6F 67 72
61 70 68 79 21

utilizzando HMAC con SHA-256 e la chiave:
0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b
0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b

Esercizio 6.7 Usando una libreria crittografica esistente, calcolate il MAC del messaggio:
4D 41 43 73 20 61 72 65 20 76 65 72 79 20 75 73
65 66 75 6C 20 69 6E 20 63 72 79 70 74 6F 67 72
61 70 68 79 21

utilizzando GMAC con AES e la chiave di 256 bit e il nonce:
80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01
00 00 00 00 00 00 00 00 00 00 00 01

Capitolo 7

Il canale sicuro

Siamo finalmente giunti al primo dei problemi reali
che risolveremo. Il canale sicuro è probabilmente il
più comune di tutti i problemi concreti.

7.1 Proprietà di un canale sicuro
In modo informale definiamo il problema come
segue: creare una connessione sicura tra Alice e Bob.
Dovremo formalizzare un po’ questo concetto per
chiarire meglio di che cosa stiamo parlando.

Sommario
7.1 Proprietà di un canale
sicuro
7.2 Ordine di autenticazione
e cifratura
7.3 Progettazione di
un canale sicuro:
panoramica
7.4 Dettagli di progettazione
7.5 Alternative

7.1.1 I ruoli
Innanzitutto, le connessioni sono per la maggior
parte bidirezionali. Alice invia dei messaggi a Bob,
e Bob invia dei messaggi ad Alice. I due flussi di
messaggi non devono essere confusi, perciò occorre
che nel protocollo sia presente una certa asimmetria.
Nei sistemi reali, una delle parti potrebbe essere il
client e l’altra il server, o forse è più semplice parlare
di chiamante (la parte che ha iniziato la connessione
sicura) e chiamato. Non importa il metodo adottato,
ma è necessario assegnare ad Alice e Bob i ruoli delle
due parti in questione, in modo che ciascuno sia
consapevole del ruolo che deve rivestire.
Naturalmente c’è sempre Eve, che cerca in tutti i
modi di attaccare il canale sicuro. Eve può leggere
tutte le comunicazioni tra Alice e Bob e manipolarle
in maniera arbitraria. In particolare, è in grado di
cancellare, aggiungere e modificare i dati trasmessi.

7.6 Esercizi

94   Capitolo 7

Continuiamo a parlare di trasmissione di dati tra Alice e Bob, e per la maggior parte
del tempo la nostra immagine mentale è quella di due computer separati che si inviano
messaggi attraverso una rete di qualche tipo. Un’altra applicazione molto interessante
riguarda la memorizzazione sicura dei dati. Se la considerate come una trasmissione dei
dati verso il futuro, tutti i discorsi fatti qui rimarranno validi. Alice e Bob potrebbero
essere la stessa persona, e il mezzo di trasmissione potrebbe essere un nastro di backup
o una chiavetta USB. Anche in questo caso si vorrebbe proteggere il mezzo da sguardi
indiscreti e manipolazioni. Naturalmente, quando si trasmettono dati nel tempo non è
possibile utilizzare un protocollo interattivo, dato che il futuro non può inviare dati al
passato.

7.1.2 La chiave
Per realizzare un canale sicuro occorre un segreto condiviso. In questo caso supporremo
che Alice e Bob condividano una chiave segreta K, che nessun altro conosce. Questa è
una proprietà essenziale. Le primitive crittografiche non possono mai identificare Alice
come persona, al massimo possono identificare la chiave. Così l’algoritmo di verifica di
Bob gli comunicherà qualcosa come:“Questo messaggio è stato inviato da qualcuno che
conosce la chiave K e che ha impersonato il ruolo di Alice”. Questa affermazione è utile
solo se Bob sa che la conoscenza di K è limitata, preferibilmente a se stesso e ad Alice.
Non importa, qui, il modo in cui la chiave sia stabilita. Partiamo semplicemente dal
presupposto che sia presente. Nel Capitolo 14 esamineremo a fondo i meccanismi di
gestione delle chiavi.
Ecco i requisiti per la chiave:
• la chiave K è nota soltanto ad Alice e a Bob;
• ogni volta che il canale sicuro viene inizializzato, viene generato un nuovo valore
di K.
Anche il secondo requisito è importante. Se la stessa chiave fosse usata di continuo, i
messaggi delle vecchie sessioni potrebbero essere reinviati ad Alice o Bob, portando a
una grande confusione. Pertanto, anche in situazioni dove la chiave è rappresentata da
una password fissa, è necessario un protocollo di negoziazione delle chiavi tra Alice e
Bob che consenta di stabilire un’unica chiave K adatta, e questo protocollo deve essere
ripetuto ogniqualvolta si instaura un canale sicuro. Una chiave come K che viene usata
per una sola comunicazione viene chiamata chiave di sessione. Anche il modo in cui K
viene generata verrà trattato nel Capitolo 14.
Il canale sicuro è progettato per raggiungere un livello di sicurezza di 128 bit. In conformità a quanto discusso nel Paragrafo 3.5.7, useremo una chiave di 256 bit. Pertanto,
K è un valore di 256 bit.

7.1.3 Sequenza o flusso
A questo punto ci si chiede se le comunicazioni tra Alice e Bob debbano essere viste
come una sequenza di messaggi discreti (come per l’email) o come un flusso continuo di
byte (come nello streaming multimediale). Prenderemo in considerazione soltanto sistemi
che gestiscono messaggi discreti. Questi sistemi possono essere facilmente convertiti in
modo da poter gestire un flusso di dati, suddividendo quest’ultimo in messaggi separati e

Il canale sicuro   95

riassemblandolo una volta raggiunto il destinatario. In realtà quasi tutti i sistemi, a livello
della crittografia, usano messaggi discreti.
Supporremo anche che il sistema di trasporto sottostante che trasmette i messaggi tra
Alice e Bob non sia affidabile. Perfino un protocollo di comunicazione affidabile come
TCP/IP non può essere considerato un canale di comunicazione affidabile da un punto
di vista crittografico. Dopo tutto, l’attaccante può facilmente modificare, rimuovere o
aggiungere dati in un flusso TCP senza interromperlo.TCP è affidabile solo per quanto
riguarda eventi casuali come la perdita di pacchetti, ma non protegge da un avversario
attivo. Da questo punto di vista, non esiste nulla che possa essere definito un protocollo
di comunicazione affidabile (ecco un buon esempio di come i crittografi vedono il
mondo).

7.1.4 Proprietà di sicurezza
Ora possiamo formulare le proprietà di sicurezza del canale. Alice invia una sequenza
di messaggi m1, m2, ... che vengono elaborati dagli algoritmi del canale sicuro e quindi
inoltrati a Bob. Bob elabora i messaggi ricevuti attraverso il canale sicuro e ottiene una
sequenza di messaggi m1, m2, ... .
Devono valere le seguenti proprietà.
• Eve non può ricavare informazioni sui messaggi mi, a parte i tempi di invio e le
dimensioni.
• Anche se Eve attacca il canale manipolando i dati in trasmissione, la sequenza m’1,
m’2, ... dei messaggi ricevuta da Bob è una sottosequenza di m1, m2, ..., e Bob può
sapere con esattezza quale sottosequenza ha ricevuto (definiamo una sottosequenza
come una sequenza da cui sono stati rimossi zero o più elementi).
La prima proprietà è la segretezza. Idealmente, Eve non dovrebbe essere in grado di
venire a sapere nulla sui messaggi. Nella vita reale è molto difficile raggiungere questo
obiettivo. È estremamente complicato nascondere informazioni come le dimensioni o i
tempi di invio. Le soluzioni note richiedono che Alice invii un flusso continuo di messaggi sfruttando la massima larghezza di banda possibile. Se non ha messaggi da inviare,
dovrebbe inventarne alcuni fasulli. Questo potrebbe essere accettabile per applicazioni
militari, ma non lo è per la maggior parte delle applicazioni civili. Posto che Eve sia
in grado di conoscere dimensioni e tempistiche dei messaggi inviati su un canale di
comunicazione, sarà anche in grado di sapere chi sta comunicando con chi, per quanto
e quando. Tutto ciò viene definito analisi del traffico, è un’attività che rivela molte informazioni ed è estremamente difficile da impedire. Si tratta di un problema noto anche
per altri canali sicuri, come SSL/TLS, IPsec e SSH. Non lo risolveremo in questo libro,
perciò Eve potrà condurre la sua analisi del traffico sul nostro canale sicuro.
La seconda proprietà assicura che Bob riceva solamente messaggi corretti, e che li riceva
nel giusto ordine. Idealmente si vorrebbe che Bob ricevesse l’esatta sequenza di messaggi
che Alice ha inviato. Nessuno dei protocolli di comunicazione reali, però, è affidabile dal
punto di vista crittografico. Eve può sempre eliminare un messaggio in transito. Dato
che non possiamo evitare la perdita di messaggi, Bob dovrà accontentarsi di riceverne
solamente una sottosequenza. Da notare che i messaggi che Bob riceve sono nel giusto
ordine; non ci sono messaggi duplicati o modificati, non ci sono messaggi contraffatti
inviati da qualcuno che non sia Alice. Come ulteriore requisito, Bob dovrà sapere quali
Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

96   Capitolo 7

messaggi non ha ricevuto. Questo può essere importante in alcune operazioni dove
l’interpretazione dei messaggi dipende dall’ordine in cui sono stati ricevuti.
Nella gran parte delle situazioni, Alice vuole assicurarsi che Bob riceva tutte le informazioni che gli ha inviato. Svariati sistemi implementano uno schema per cui Bob
invia conferme (implicite o esplicite) ad Alice, che a sua volta provvede a reinviare le
informazioni per le quali non le sia giunta conferma. Notate che il nostro canale sicuro
non prende mai l’iniziativa di reinviare un messaggio. Alice dovrà provvedere da sola, o
quanto meno lo dovrà fare il livello di protocollo che si serve del canale sicuro.
E allora perché non rendere affidabile il canale sicuro implementando al suo interno la
funzionalità di reinvio? Perché così facendo aumenteremmo la complessità della descrizione del canale. È bene mantenere semplici i moduli critici per la sicurezza. Le conferme
di messaggi e reinvii sono tecniche standard dei protocolli di comunicazione, e possono
essere implementate a un livello superiore rispetto al canale sicuro. Inoltre, questo è un
libro sulla crittografia, non sulle tecniche di base dei protocolli di comunicazione.

7.2 Ordine di autenticazione e cifratura
Ovviamente, al messaggio verranno applicate sia la cifratura che l’autenticazione. Al
riguardo possiamo individuare tre approcci [7, 82]: si può prima cifrare e quindi autenticare il testo cifrato (“cifratura anticipata”); si può eseguire prima l’autenticazione e
quindi cifrare sia il messaggio che il valore MAC (“autenticazione anticipata”); oppure
si può cifrare e autenticare il messaggio e poi combinare (per esempio concatenandoli) i
risultati ottenuti (“cifratura e autenticazione contemporanee”). Non è semplice indicare
quale sia il metodo migliore.
Gli argomenti a favore dell’approccio con cifratura anticipata sono due. Esistono risultati
teorici che mostrano che, date particolari definizioni specifiche di cifratura e autenticazione sicura, la soluzione che prevede prima la cifratura e poi l’autenticazione è sicura,
mentre gli altri approcci non lo sono. Osservando i dettagli si vede che l’approccio con
autenticazione anticipata non è sicuro solo se lo schema di cifratura presenta un tipo
specifico di vulnerabilità. Nei sistemi reali non vengono mai utilizzati sistemi con simili
vulnerabilità. In ogni caso, questi schemi di cifratura deboli soddisfano una particolare
definizione formale di sicurezza. Applicando il MAC al testo cifrato di uno schema di
cifratura debole, si apporta una correzione che fornisce sicurezza. Avere a disposizione
questi risultati teorici è utile, tuttavia essi non sono sempre validi per gli schemi di cifratura reali. In effetti, vi sono dimostrazioni del fatto che questi problemi non si verificano
affatto per i cifrari a blocchi (come la modalità CTR) e la cifratura a modalità CBC
quando siano autenticati il nonce o l’IV.
Il secondo argomento a favore dell’approccio con cifratura anticipata è la sua efficienza
nello scartare i messaggi falsificati. Per i messaggi normali, Bob deve sia decifrare il messaggio sia verificare l’autenticazione, indipendentemente dall’ordine in cui sono state
applicate in origine. Se il messaggio è fasullo (cioè se ha un campo MAC erroneo) allora
Bob lo scarterà. Con l’approccio che esegue prima la cifratura, la decifratura viene svolta
per ultima dal lato del chiamato, e Bob non dovrà mai decifrare messaggi fasulli, dato che
può identificarli e scartarli prima della decifratura. Con l’approccio che esegue prima
l’autenticazione, Bob deve decifrare il messaggio prima di verificare l’autenticazione, e
questo comporta un maggiore lavoro per la gestione dei messaggi fasulli. La situazione in
Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

Il canale sicuro   97

cui tutto ciò si rivela utile è quella in cui Eve invia a Bob un numero veramente grande
di messaggi fasulli. L’approccio con cifratura anticipata consente a Bob di risparmiare
l’attività di decifratura, riducendo il carico della CPU. In alcune particolari circostanze,
ciò consente di rendere un po’ più complicato un attacco DoS (Denial of Service), anche
se solo di un fattore due, al massimo. Inoltre, in molte situazioni reali, un attacco DoS
più efficiente lavora saturando il canale di comunicazione, e non appesantendo la CPU
di Bob.
Il principale argomento a favore dell’approccio “cifratura e autenticazione contemporanee” è che i processi coinvolti possono verificarsi in parallelo. In alcune situazioni
questo migliora le prestazioni. Con l’approccio che prevede la cifratura anticipata, un
attaccante può vedere il tag MAC del messaggio iniziale. Questo perché il MAC non è
cifrato (diversamente da quanto avviene nella modalità con autenticazione anticipata)
e perché il MAC non fa riferimento a un valore cifrato (diversamente dall’approccio
con cifratura anticipata). I MAC sono stati progettati per proteggere l’autenticità, non
la riservatezza. Questo significa che un MAC in un approccio con cifratura anticipata
potrebbe lasciare filtrare informazioni private sul messaggio sottostante, compromettendo
così la riservatezza del canale sicuro. Come nel caso dell’autenticazione anticipata, esistono
alcuni schemi di cifratura che non sono sicuri se utilizzati in un approccio con cifratura
anticipata. Con una scelta oculata del MAC e dello schema di cifratura sottostanti, e
inserendo dati aggiuntivi come il nonce nell’input per il MAC, l’approccio con cifratura
e autenticazione contemporanee può essere sicuro.
Vi sono due argomenti principali a favore dell’approccio con autenticazione anticipata.
Nell’approccio con cifratura anticipata, l’input per il MAC e il valore del MAC sono
entrambi visibili da Eve. Nell’approccio con autenticazione anticipata, Eve riesce a vedere
solo il testo cifrato e il valore cifrato del MAC; l’input per il MAC (il testo in chiaro) e
l’effettivo valore del MAC sono nascosti. Un attacco al MAC, in questo modo, diventa
più difficile rispetto al caso della cifratura anticipata. La scelta vera riguarda quale delle
due funzioni debba essere applicata per ultima. Se si applica per ultima la cifratura, allora
Eve riuscirà ad attaccare la funzione di cifratura senza ulteriori impedimenti. Se viene
applicata per ultima l’autenticazione, Eve potrà attaccare senza problemi la funzione di
autenticazione. In molti casi, si potrebbe sostenere che l’autenticazione sia più importante
della cifratura. Pertanto preferiamo esporre la funzione di cifratura agli attacchi e proteggere il MAC il più possibile. Ovviamente questi discorsi perdono di significato se lo
schema di cifratura e il MAC sono entrambi sicuri, ma stiamo adottando un atteggiamento
di paranoia professionale e vorremmo avere un canale sicuro che sia ragionevolmente
robusto anche in mancanza di tale ipotesi.
In quale caso l’autenticazione può essere più importante della cifratura? Immaginate una
situazione in cui si sta utilizzando un canale sicuro. Pensate a quanti danni Eve potrebbe
provocare se fosse in grado di leggere tutto il traffico. Poi pensate a quanti danni potrebbe
provocare se riuscisse a modificare i dati trasmessi. Nella gran parte dei casi, la modifica
dei dati costituisce un attacco devastante e fa più danni di una semplice lettura.
Il secondo argomento a favore dell’autenticazione anticipata è il principio di Horton. Si
deve autenticare il significato, non le parole con cui è espresso. L’autenticazione del testo
cifrato infrange tale principio e provoca una vulnerabilità. Il pericolo è che Bob potrebbe
verificare che il testo sia correttamente autenticato, ma poi decifrarlo con una chiave
diversa da quella usata da Alice per cifrarlo. Bob otterrà così un testo in chiaro diverso
da quello inviato da Alice, anche se la verifica sull’autenticazione ha dato esito positivo.

98   Capitolo 7

Questo non dovrebbe succedere, eppure può capitare. Esiste una particolare (insolita)
configurazione di IPsec che presenta questo problema [51]. Questa vulnerabilità deve
essere corretta. Si potrebbe inserire la chiave di cifratura nei dati aggiuntivi che vengono
autenticati, ma non ci piace utilizzare le chiavi al di fuori del loro ruolo consueto, poiché
così ci si espone a rischi ulteriori; non vogliamo che una funzione di MAC difettosa
riveli informazioni sulla chiave. La soluzione standard è quella di derivare sia la chiave
di cifratura che la chiave di autenticazione per il canale sicuro da una singola chiave del
canale sicuro, come nel Paragrafo 7.4.1. Così si rimuove la vulnerabilità, ma si introduce
una dipendenza incrociata. Improvvisamente l’autenticazione dipende dal sistema di
derivazione della chiave. Si potrebbe discutere per ore su quale sia l’ordine migliore per
le operazioni.Tutti gli approcci possono produrre sistemi buoni e non, ciascuno presenta
vantaggi e svantaggi. Nel prosieguo di questo capitolo opteremo per l’autenticazione
anticipata, poiché ci piace la semplicità di tale soluzione e la sua sicurezza rispetto al
nostro modello di paranoia professionale.

7.3 Progettazione di un canale sicuro: panoramica
La soluzione è costituita da tre componenti: numerazione dei messaggi, autenticazione e
cifratura. Seguiremo passo passo la progettazione di un canale sicuro e, durante il percorso,
illustreremo come affrontare i vari aspetti.

7.3.1 Numerazione dei messaggi
I numeri dei messaggi sono vitali per vari motivi. Forniscono una sorgente di IV per
l’algoritmo di cifratura; permettono a Bob di respingere i messaggi reinviati senza necessità di ricorrere a un database di grandi dimensioni; indicano a Bob i messaggi andati
persi nel transito; garantiscono che Bob riceva i messaggi nel giusto ordine. Per questi
motivi, la numerazione dei messaggi deve essere monotona crescente (cioè i messaggi
più recenti dovranno avere numeri più alti) e deve essere univoca (non possono esserci
due messaggi con lo stesso numero).
Assegnare i numeri ai messaggi è semplice. Alice assegna al primo messaggio il numero
1, al secondo messaggio il numero 2 e così di seguito. Bob tiene traccia del numero
dell’ultimo messaggio ricevuto. Ogni nuovo messaggio dovrà avere un numero più alto
di quello del messaggio precedente. Accettando solo numeri di messaggio crescenti, Bob
si assicura che Eve non gli possa reinviare un vecchio messaggio.
Per il nostro progetto di canale sicuro, useremo per numerare i messaggi un numero di 32
bit. Al primo messaggio viene assegnato il numero 1. Il numero dei messaggi è limitato
a 232 − 1. Se il numero dei messaggi viene superato, Alice dovrà cessare di utilizzare la
chiave corrente e rieseguire il protocollo di negoziazione delle chiavi per generarne una
nuova. Il numero dei messaggi deve essere unico, non è possibile che torni a 0.
Avremmo potuto utilizzare un numero di 64 bit, ma ciò comporterebbe un notevole
sovraccarico (si dovrebbero includere in ogni messaggio 8 byte per il suo numero, invece
di soli 4 byte). 32 è un valore adatto per la gran parte delle applicazioni. Inoltre, la chiave
dovrebbe comunque essere cambiata regolarmente (tutte le chiavi dovrebbero essere
rinnovate a intervalli ragionevoli. Le chiavi più utilizzate dovrebbero essere aggiornate

Il canale sicuro   99

più spesso. Limitare l’uso di una chiave a un numero di 232 − 1 messaggi). Volendo,
ovviamente, potete usare 40 o 48 bit; non cambia molto.
Perché cominciare da 1 quando la gran parte dei programmatori C preferisce iniziare
da 0? È un piccolo trucco di implementazione. Se possono essere assegnati N numeri,
allora Alice e Bob devono essere in grado di tenere traccia di N + 1 stati. Dopo tutto, il
numero di messaggi inviati fino a un dato momento potrebbe essere un valore qualsiasi
dell’intervallo {0, ... , N}. Fissando un limite a 232 − 1 messaggi, questo stato può essere
codificato in un singolo valore di 32 bit. Se avessimo iniziato a numerare i messaggi da 0,
ogni implementazione richiederebbe un flag aggiuntivo per indicare che fino a un dato
momento non è stato inviato alcun messaggio, o che l’intervallo dei numeri disponibili
si è esaurito. Questo tipo di flag aggiuntivi richiede l’inserimento di codice complicato
e pesante che viene eseguito molto di rado. Se è usato raramente, probabilmente sarà
stato testato in maniera ancora più sporadica, e pertanto molto probabilmente non
funzionerà. In breve, facendo partire la numerazione da 1 possiamo eliminare tutta una
serie di possibili problemi.
Per la parte restante di questo capitolo il numero di messaggio verrà indicato con i.

7.3.2 Autenticazione
Ci serve un MAC per la funzione di autenticazione. Come potreste aspettarvi, useremo
HMAC-SHA-256 con l’intero risultato di 256 bit. L’input per il MAC è formato dal
messaggio mi e dai dati aggiuntivi di autenticazione xi. Come abbiamo spiegato nel Capitolo 6, spesso nell’autenticazione devono essere inseriti alcuni dati di contesto. Questo
è il contesto che Bob utilizzerà per interpretare il significato del messaggio; in genere i
dati riguardano il protocollo, il numero di versione del protocollo e le dimensioni dei
campi stabilite. Qui si sta solamente specificando il canale sicuro; il valore effettivo di xi
dovrà essere fornito dal resto dell’applicazione. Dal nostro punto di vista, ogni xi è una
stringa, e sia Alice che Bob conoscono lo stesso valore di xi.
Sia ℓ(·) la funzione che restituisce la lunghezza (in byte) di una stringa di dati. Il valore
MAC a viene calcolato come:
ai := MAC(i || ℓ(xi) xi || mi)
dove i e ℓ(xi) sono entrambi interi di 32 bit senza segno nel formato LSB (Least Significant Byte First, con il byte meno significativo per primo). La funzione ℓ(xi) assicura che
la stringa i || (xi) || xi || mi venga analizzata nei relativi campi in maniera univoca. Senza
ℓ(xi), ci sarebbero molti modi per suddividerla in i, xi, e mi, e di conseguenza l’autenticazione non sarebbe inequivocabile. Naturalmente xi dovrebbe essere codificato in modo
tale da potere essere analizzato per individuarne i campi senza ulteriori informazioni
di contesto, ma questo è un requisito che non possiamo soddisfare a questo livello. La
richiesta dovrà essere garantita dall’applicazione che utilizza il canale sicuro.

7.3.3 Cifratura
Per la cifratura useremo AES in modalità CTR. Ma un momento, nel Paragrafo 4.7
non avevamo detto che la modalità CTR è pericolosa per via del nonce? Sì, l’abbiamo
detto... più o meno. In effetti abbiamo detto che rivelare i meccanismi di controllo del
Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

100   Capitolo 7

nonce agli sviluppatori è rischioso, e che abbiamo visto troppe applicazioni dimostratesi
insicure perché non in grado di generare il nonce nella maniera corretta. Il nostro canale
sicuro, però, gestisce il nonce internamente, senza mai assegnare ad altri il controllo sulla
sua generazione. Usiamo il numero di messaggio come unico valore nonce di cui la
modalità CTR ha bisogno. Pertanto il nostro canale sicuro utilizza la modalità CTR. Ciò
non toglie che non vorremmo mai rivelare la generazione del nonce a sistemi esterni.
Raccomandiamo di non usare mai direttamente la modalità CTR.
Limitiamo le dimensioni di ogni messaggio a 16 ∙ 232 byte, e questo limita il contatore
di blocco a 32 bit. Ovviamente potremmo utilizzare un contatore di 64 bit, ma 32 bit
possono essere implementati più facilmente su diverse piattaforme, e la gran parte delle
applicazioni non ha necessità di elaborare messaggi di questa dimensione.
Il flusso chiave è costituito dai byte k0, k1, ... Per un messaggio con un nonce i, il flusso
chiave è definito da:
k0, ... , k236−1
:= E(K,0 || i || 0) || E(K,1 || i || 0) ··· || E(K, 232 − 1 || i || 0)
dove ogni blocco di testo in chiaro del cifrario viene costruito da un numero di blocco
di 32 bit, il numero di messaggio di 32 bit e 64 bit di zeri. Il flusso chiave è una stringa
molto lunga; ne useremo solamente i primi ℓ(mi) + 32 byte (non è il caso di ricordare
che non è necessario calcolare la parte restante del flusso chiave...). Concateniamo mi e
ai e ne calcoliamo l’xor con k0, ..., kl(mi)+31.

7.3.4 Formato del frame
Non possiamo limitarci a inviare il messaggio cifrato mi || ai, perché Bob deve conoscere
il numero di messaggio. L’ultimo messaggio inviato sarà formato da i codificato come
un intero di 32 bit, con il byte meno significativo per primo, seguito dalla codifica di
mi e ai.

7.4 Dettagli di progettazione
Ora possiamo discutere i dettagli del canale sicuro. Ancora una volta sottolineiamo che
quello presentato qui non è l’unico modo per implementare un canale sicuro, ma piuttosto
un’opportunità per immergerci tra le sfide e le sfumature che si incontrano in questa
attività. Per comodità abbiamo specificato che per definizione il canale è bidirezionale,
perciò la medesima chiave potrà essere utilizzata per entrambi i versi. Se definissimo un
canale monodirezionale, potremmo essere certi che qualcuno finirebbe per usare la stessa
chiave in entrambe le direzioni, con un effetto devastante per la sicurezza. Rendere il
canale bidirezionale riduce questo rischio. D’altro canto, se utilizzate un canale sicuro
definito da altri, prestate ancora più attenzione a non usare la stessa chiave per entrambe
le direzioni.
Descriveremo tutti i nostri algoritmi con una notazione di pseudocodice che dovrebbe
risultare di facile lettura per chiunque abbia una certa familiarità con le convenzioni di
programmazione. I blocchi dei programmi sono contrassegnati dai livelli di rientro e da
coppie di parole chiave come if/fi e do/od.

Il canale sicuro   101

7.4.1 Inizializzazione
Il primo algoritmo che illustriamo è l’inizializzazione dei dati del canale. Le sue funzioni
principali sono due: impostare le chiavi e impostare la numerazione dei messaggi. Deriviamo quattro chiavi secondarie dalla chiave del canale: una chiave di cifratura e una
chiave di autenticazione per l’invio dei messaggi da Alice a Bob, e una chiave di cifratura
e una di autenticazione per l’invio nel senso contrario.
function InitializeSecureChannel
input: K Chiave del canale, 256 bit.

R Ruolo. Specifica l’interlocutore, Alice o Bob.
output: S Stato del canale sicuro.

Innanzitutto calcola le quattro chiavi necessarie. Sono quattro stringhe ASCII senza lunghezza o terminatore zero.

KeySendEnc ← SHAd-256(K || “Enc Alice to Bob”)

KeyRecEnc ← SHAd-256(K || “Enc Bob to Alice”)

KeySendAuth ← SHAd-256(K || “Auth Alice to Bob”)

KeyRecAuth ← SHAd-256(K || “Auth Bob to Alice”)

Se l’interlocutore è Bob, scambia le chiavi di cifratura e decifratura.

if R = “Bob” then

swap(KeySendEnc, KeyRecEnc)

swap(KeySendAuth, KeyRecAuth)

fi

Imposta a zero i contatori di invio e ricezione. Il contatore di invio è il numero dell’ultimo
messaggio inviato. Il contatore di ricezione è il numero dell’ultimo messaggio
ricevuto.

(MsgCntSend, MsgCntRec) ← (0, 0)

Impacchetta lo stato.

S ← (KeySendEnc,

KeyRecEnc,

KeySendAuth,

KeyRecAuth,

MsgCntSend,

MsgCntRec)

return S
Esiste anche una funzione per cancellare le informazioni di stato S. Non la descriveremo
in dettaglio. Il suo compito è semplicemente quello di cancellare la memoria utilizzata
da S per registrare le informazioni. È vitale che queste informazioni vengano eliminate,
perché è qui che le chiavi erano memorizzate. In molti sistemi, la sola deallocazione della
memoria non ne implica la pulizia, perciò conviene eliminare S quando non serve più.

102   Capitolo 7

7.4.2 Invio di un messaggio
Passiamo ora alle elaborazioni necessarie per inviare un messaggio. Questo algoritmo
riceve in input lo stato della sessione, un messaggio da inviare e dati aggiuntivi che
devono essere autenticati, e produce il messaggio cifrato e autenticato pronto per la
trasmissione. Il ricevente deve avere la disponibilità degli stessi dati aggiuntivi per verificare l’autenticazione.
function SendMessage
input: S Stato della sessione sicura.

m Messaggio da inviare.

x Dati aggiuntivi da autenticare.
output: t Dati da trasmettere al destinatario.

Innanzitutto controlla il numero di messaggio e lo aggiorna.

assert MsgCntSend < 232 − 1

MsgCntSend ← MsgCntSend + 1

i ← MsgCntSend

Calcola l’autenticazione. I valori ℓ(x) e i vengono codificati in quattro byte, con il meno
significativo per primo.

a ← HMAC-SHA-256(KeySendAuth, i || ℓ(x) || x || m)

t ← m || a

Genera il flusso chiave. Ogni blocco di testo in chiaro del cifrario a blocchi è formato da un
contatore di quattro byte, quattro byte di i e otto byte di zeri. Gli interi sono in
LSB, E è la cifratura AES con una chiave di 256 bit.

K ← KeySendEnc

k ← EK(0 || i || 0) || EK(1 || i || 0) || ···

Forma il testo finale. Di nuovo, i viene codificato come quattro byte, LSB.

t ← i || (t ⊕ First-ℓ(t)-bytes(k))

return t
Alla luce delle considerazioni precedenti, è tutto relativamente semplice. Verifichiamo
la saturazione del contatore dei messaggi. Non potremo mai sottolineare a sufficienza
l’importanza di questo controllo; se il contatore dovesse ripartire dall’inizio, tutta la sicurezza andrebbe perduta, ed è un errore che abbiamo visto molto spesso. Autenticazione
e cifratura sono eseguite secondo quanto visto precedentemente. Infine, inviamo i con
il messaggio cifrato e autenticato in modo che il ricevente potrà conoscere il numero
di messaggio.
Notate che lo stato della sessione viene aggiornato perché il valore di MsgCntSend
viene modificato. Ancora una volta ripetiamo che questo è di vitale importanza, dato
che il numero di messaggio deve essere unico. In effetti, quasi tutto in questi algoritmi
è vitale per la sicurezza.
Il nostro canale sicuro utilizza la modalità CTR per la cifratura. Se lo schema di cifratura
richiede il riempimento, accertatevi di verificarne i contenuti in fase di decifratura.

Il canale sicuro   103

7.4.3 Ricezione di un messaggio
L’algoritmo di ricezione richiede il messaggio cifrato e autenticato prodotto da SendMessage e gli stessi dati aggiuntivi x per l’autenticazione. Partiamo dal presupposto che il
ricevente sia a conoscenza di x. Per esempio, se x contiene il numero di versione del
protocollo, Bob lo dovrà sapere, dato che sta partecipando.
function ReceiveMessage
input: S Stato della sessione sicura.

t Testo ricevuto dal mittente.

x Dati aggiuntivi per l’autenticazione.
output: m Messaggio inviato.

Il messaggio ricevuto deve contenere quantomeno un numero di messaggio di 4 byte e un
campo MAC di 32 byte. Questa verifica assicura il funzionamento di tutte le
successive operazioni di suddivisione.

assert ℓ(t) ≥ 36

Suddivide t in i e il messaggio cifrato più i dati di autenticazione. La suddivisione è ben
definita perché la lunghezza di i è sempre di 4 byte.

i || t ← t

Genera il flusso chiave, proprio come il mittente.

K ← KeyRecEnc

k ← EK(0 || i || 0) EK(1 || i || 0) ···

Decifra il messaggio e il campo MAC, e suddivide. La suddivisione è ben definita perché la
lunghezza di a è sempre di 32 byte.

m || a ← t ⊕ First-ℓ(t)-bytes(k)

Ricalcola l’autenticazione. I valori l(x) e i sono cifrati in quattro byte, con il meno significativo
all’inizio.

a' ← HMAC-SHA-256(KeyRecAuth, i || ℓ(x) || x || m)

if a' ≠ a then

destroy k, m

return AuthenticationFailure

else if i ≤ MsgCntRec then

destroy k, m

return MessageOrderError

fi

MsgCntRec ← i

return m
In questo caso abbiamo usato l’ordine delle operazioni canonico. Potremmo spostare il
controllo sul numero di messaggio prima della decifratura, ma la funzione riporterebbe
l’errore sbagliato se i venisse modificato durante la trasmissione; invece di informare il
mittente della modifica apportata al messaggio, gli indicherebbe che il messaggio non si
trova nell’ordine corretto. Dato che il mittente potrebbe preferire una gestione diversa
delle due situazioni, questa routine non dovrebbe fornire informazioni errate. Il motivo

104   Capitolo 7

per cui qualcuno preferisce anticipare il controllo è che in questo modo è possibile scartare più velocemente i falsi messaggi. A noi questo non sembra troppo importante: se si
riceve un numero di pacchetti falsi tale che la velocità con cui vengono scartati diventa
un fattore significativo, significa che ci sono già problemi molto più gravi
Per il destinatario esiste solo una questione molto importante. La funzione ReceiveMessage potrebbe non rilasciare alcuna informazione sul flusso chiave o sul messaggio
in chiaro fino a che l’autenticazione non sia stata verificata. Se l’autenticazione fallisce,
viene restituita una segnalazione al riguardo, ma né il flusso chiave né il testo in chiaro
possono essere rivelati. Un’implementazione reale dovrebbe cancellare le aree di memoria
utilizzate per salvare questi elementi. Perché è così importante? Il messaggio in chiaro
rivela il flusso chiave, dato che si presume che ogni attaccante conosca il testo cifrato. Il
pericolo è che l’attaccante invii un messaggio contraffatto (con un valore MAC errato)
ma riesca ancora a ricavare il flusso chiave dai dati rilasciati dal destinatario. Siamo ancora nel modello della paranoia. Si presume che ogni dato rilasciato o fatto trapelare da
questa routine finisca automaticamente in possesso dell’attaccante. Distruggendo i dati
memorizzati in k e m prima di restituire un errore, la routine si assicura che essi non
potranno mai trapelare.

7.4.4 Ordinamento dei messaggi
Come il mittente, il destinatario aggiorna lo stato S modificando la variabile MsgCntRec. Il destinatario si assicura che i numeri dei messaggi che accetta siano in continuo
aumento. Ciò garantisce che nessun messaggio possa venire accettato due volte, ma se il
flusso viene riordinato durante la trasmissione, messaggi che altrimenti sarebbe perfettamente validi andranno invece persi.
Questo problema può essere risolto in maniera relativamente semplice, ma a un prezzo.
Se si consente che il destinatario accetti messaggi non ordinati, l’applicazione che utilizza il canale sicuro dovrà essere in grado di gestirli. Molte applicazioni non lo sono.
Alcune applicazioni sono progettate per questo tipo di gestione, ma presentano piccoli
bug (spesso di sicurezza) con i messaggi riordinati. In genere preferiamo correggere il
livello di trasporto sottostante e impedire il riordinamento accidentale dei messaggi, in
modo che il canale sicuro non debba occuparsi del problema.
Conosciamo un caso in cui il destinatario consente l’arrivo disordinato dei messaggi, e per
un’ottima ragione. Si tratta di IPsec, il protocollo di sicurezza IP [73] che cifra e autentica
i pacchetti. Dato che i pacchetti IP possono essere riordinati durante il trasporto, e dato
che tutte le applicazioni che usano IP sono al corrente di questa proprietà, IPsec mantiene
una finestra di protezione dalla ripetizione dei messaggi anziché limitarsi a ricordare il
valore di contatore dell’ultimo messaggio ricevuto. Se c è il numero dell’ultimo messaggio
ricevuto, IPsec mantiene una mappa di bit per i numeri di messaggio c − 31, c − 30, c −
29, ... , c − 1, c. Ogni bit indica se un messaggio con il numero corrispondente è stato
ricevuto. I messaggi con numeri inferiori a c − 31 vengono sempre rifiutati. I messaggi
nell’intervallo tra c − 31 e c − 1 vengono accettati solo se il bit corrispondente è 0 (e il
bit viene inviato, ovviamente). Se il nuovo messaggio ha un numero superiore a c, allora
c viene aggiornato e si effettua uno shift della mappa di bit per mantenere l’invariante.
Una mappa di bit costruita in questo modo consente un riordinamento limitato dei
messaggi senza aumentare eccessivamente i dati di stato per il destinatario.
Un’altra possibilità è quella di chiudere le comunicazioni nel caso in cui un messaggio
venga scartato. Questa soluzione è particolarmente adatta quando il canale sicuro si

Il canale sicuro   105

appoggia a un trasporto ideale come TCP. Se non interviene alcuna attività malevola, i
messaggi dovrebbero arrivare in ordine e senza alcuna perdita. Perciò, se un messaggio
viene scartato o non arriva nell’ordine corretto, chiudete le comunicazioni.

7.5 Alternative
La definizione di canale sicuro fornita qui non è sempre pratica; soprattutto quando si
realizza un canale sicuro in dispositivi hardware embedded, diventa relativamente oneroso implementare SHA-256. Come alternativa, si è recentemente evidenziato un certo
interesse nella creazione di modalità dei cifrari a blocchi dedicate, che garantiscano al
contempo riservatezza e autenticità.
Questi cifrari a blocchi dedicati accettano in input una singola chiave, come la modalità
CBC e CBC-MAC. Queste modalità in genere accettano in input anche un messaggio, dei
dati di autenticazione e un nonce. La difficoltà, però, è superiore rispetto all’utilizzo delle
modalità CBC e CBC-MAC con la stessa chiave. L’uso della medesima chiave per una normale modalità di cifratura e per un normale MAC può portare a problemi di sicurezza.
La modalità combinata più conosciuta è OCB [109]. È molto efficiente. Ciascun blocco di
testo in chiaro può essere elaborato in parallelo, il che risulta attraente per i sistemi hardware
a grande velocità. La presenza di brevetti ha limitato l’adozione della modalità OCB.
A causa dei problemi di brevetto relativi alla modalità OCB, e spinti dalla necessità di
una modalità di cifrario a blocchi a chiave singola dedicata per cifratura e autenticazione, Doug Whiting, Russ Housley e Niels Ferguson hanno sviluppato una modalità
denominata CCM [126]. Si tratta di una combinazione di cifratura in modalità CTR e
autenticazione CBC-MAC, che però consente l’utilizzo della medesima chiave sia per
CTR che per CBC-MAC. Rispetto a OCB, questa modalità richiede un numero doppio
di calcoli per la cifratura e l’autenticazione di un messaggio, ma per quanto ci risulta non
esistono problemi di brevetto per CCM. I progettisti non sono a conoscenza di brevetti
che riguardino CCM, e non ne hanno chiesto, né chiederanno, alcuno. Jakob Jonsson
ha fornito una dimostrazione della sicurezza di CCM [65]. Il NIST nel frattempo ha
standardizzato CCM come modalità di cifrario a blocchi [41].
Per migliorare l’efficienza di CCM, Doug Whiting, JohnViega e Tadayoshi Kohno hanno
sviluppato un’altra modalità denominata CWC [80]. CWC si basa sulla modalità CTR
per la cifratura.Alla base, CWC usa un hashing universale per ottenere l’autenticità [125].
Abbiamo parlato (anche se in maniera superficiale) dell’hashing universale nel Capitolo 6
quando abbiamo presentato GMAC [43]. L’utilizzo dell’hashing universale rende CWC
pienamente parallelizzabile, come OCB, ma evita i brevetti di OCB. David McGrew e
JohnViega hanno migliorato CWC con una più efficiente funzione di hashing universale
per le implementazioni hardware. Questa modalità si chiama GCM [43] e il NIST l’ha
standardizzata come modalità di cifrario a blocchi.
Come il nostro canale sicuro descritto precedentemente, OCB, CCM, CWC e GCM
possono tutte accettare in input due stringhe: il messaggio da inviare e i dati aggiuntivi di
autenticazione. Lo schema di autenticazione dei messaggi GMAC è in effetti la modalità
GCM in cui il messaggio principale è la stringa vuota.
Tutte queste modalità costituiscono scelte ragionevoli. Personalmente preferiamo CCM
e GCM per il fatto che sono standardizzate e senza vincoli di brevetto. Sfortunatamente,
le capacità di autenticazione di GCM condividono le limitazioni di GMAC discusse nel
Paragrafo 6.5. Pertanto, per quanto sia possibile ridurre le dimensioni dell’autenticatore

106   Capitolo 7

di GCM da 128 bit a qualcosa di meno, non raccomandiamo di farlo. La nostra raccomandazione è di usare GCM solo con l’intero tag di autenticazione di 128 bit.
Un altro punto importante: OCB, CCM, CWC, GCM e le modalità simili non forniscono, da sole, un canale pienamente sicuro. Offrono la funzionalità di cifratura/autenticazione e richiedono una chiave e un nonce univoco per ogni pacchetto. Abbiamo
discusso nel Paragrafo 4.7 i rischi del fare affidamento sulle capacità di sistemi esterni
di generare correttamente i nonce. È semplice, però, adattare gli algoritmi del nostro
canale sicuro in modo che utilizzino una di queste modalità di cifrario a blocchi anziché
funzioni di MAC e di cifratura separate. Invece delle quattro chiavi secondarie generate
in InitializeSecureChannel, saranno necessarie due chiavi: una per ogni direzione del
traffico. Il nonce può essere generato con il riempimento del numero di messaggio fino
a raggiungere le dimensioni corrette.
Facendo un passo indietro, possiamo osservare che il canale sicuro è una delle applicazioni della crittografia più utilizzate, e viene usato in quasi tutti i sistemi crittografici. Si
può costruire un buon canale sicuro da buone primitive di cifratura e autenticazione,
e sono presenti anche modalità di cifrari a blocchi dedicate a cifratura e autenticazione
sulle quali basarsi. I dettagli a cui fare attenzione sono molti, e tutti devono essere gestiti
nella maniera corretta. Una sfida a parte, di cui ci occuperemo in seguito, è quella di
impostare una chiave simmetrica.

7.6 Esercizi
Esercizio 7.1 Nel nostro progetto di canale sicuro abbiamo detto che i numeri di
messaggio non devono ripetersi. Quali effetti negativi si potrebbero avere in caso di
ripetizione?
Esercizio 7.2 Modificate gli algoritmi per il canale sicuro presenti in questo capitolo in modo che venga utilizzata la modalità di cifratura anticipata per le operazioni di
cifratura e autenticazione.
Esercizio 7.3 Modificate gli algoritmi per il canale sicuro presenti in questo capitolo
in modo che venga utilizzate una modalità dedicata a chiave singola per ottenere sia cifratura che autenticazione. Potete usare come black box OCB, CCM, CWC o GCM.
Esercizio 7.4 Confrontate vantaggi e svantaggi dei diversi ordini di applicazione di
cifratura e autenticazione nella creazione di un canale sicuro.
Esercizio 7.5 Individuate un nuovo prodotto o un nuovo sistema che usa (o dovrebbe
usare) un canale sicuro. Potrebbe essere il prodotto o sistema che avete analizzato per l’Esercizio
1.8. Svolgete un’analisi della sicurezza per il prodotto o sistema come descritto nel Paragrafo
1.12, focalizzandovi sugli aspetti di sicurezza e riservatezza relativi al canale sicuro.
Esercizio 7.6 Supponete che Alice e Bob stiano usando il canale sicuro descritto in
questo capitolo per comunicare. Eve sta spiando le comunicazioni; che tipi di informazioni di analisi del traffico potrebbe essere in grado di ottenere? Descrivete una situazione
nella quale la rivelazione di informazioni tramite l’analisi del traffico costituisce un serio
problema di riservatezza.

Capitolo 8

Aspetti relativi
all’implementazione (I)
Arrivati a questo punto, ci piacerebbe parlare un po’
dei problemi di implementazione. L’implementazione dei sistemi di crittografia è abbastanza diversa da
quella dei programmi normali, tanto da meritare un
discorso a parte.
Il problema principale è, come sempre, la proprietà
dell’elemento più debole (cfr. Paragrafo 1.2). È molto
facile rovinare la sicurezza a livello dell’implementazione. In effetti, gli errori di implementazione come
il buffer overflow rappresentano uno dei maggiori
problemi di sicurezza nei sistemi reali. Capita poche
volte di sentir parlare di sistemi crittografici che
vengono compromessi nella pratica. Questo non è
dovuto al fatto che la crittografia nella gran parte
dei sistemi è molto buona; abbiamo esaminato casi a
sufficienza per sapere che non è così. Il fatto che è più
semplice individuare in un sistema una vulnerabilità
di implementazione che non una relativa alla crittografia, e gli attaccanti sono abbastanza intelligenti
da non perdere tempo con la crittografia quando
possono scegliere una strada più semplice.
Finora abbiamo limitato i nostri discorsi alla crittografia, ma in questo capitolo ci concentriamo
maggiormente sull’ambiente in cui la crittografia si
trova a operare. Ogni parte di un sistema influisce
sulla sicurezza, e per fare un lavoro davvero buono
è necessario che l’intero sistema sia progettato dalle
fondamenta non solo tenendo conto della sicurezza,
ma ponendola come uno degli obiettivi principali.
Il “sistema” di cui stiamo parlando è molto grande:
comprende tutto ciò che potrebbe danneggiare le
proprietà di sicurezza qualora esibisca un comportamento errato.

Sommario
8.1 Creazione di programmi
corretti
8.2 Creazione di software
sicuro
8.3 Mantenere i segreti
8.4 Qualità del codice
8.5 Attacchi a canale laterale
8.6 Conclusioni e altre
letture
8.7 Esercizi

108   Capitolo 8

Una delle parti principali è sempre il sistema operativo. Storicamente, però, nessuno dei
sistemi operativi più utilizzati è stato progettato pensando alla sicurezza come uno degli
obiettivi. E la diversità dei sistemi operativi è enorme, da quelli con i quali interagiamo sui
nostri computer, a quelli presenti in vari dispositivi o nei telefoni. La conclusione logica
che si può trarre da questa situazione è che è impossibile implementare un sistema sicuro.
Noi non sappiamo come farlo, e neppure conosciamo qualcuno che lo sappia fare.
I sistemi reali contengono diversi componenti che non sono mai stati progettati per la
sicurezza, e questo rende impossibile raggiungere il livello di sicurezza effettivamente necessario. Dovremmo arrenderci, allora? Ovviamente no. Quando progettiamo un sistema
crittografico, facciamo di tutto perché almeno la nostra parte sia sicura. Potrebbe sembrare
un atteggiamento mentale bizzarro: ci preoccupiamo solo del nostro piccolo regno. Ma
a noi interessano anche le altre parti del sistema; semplicemente non possiamo fare nulla
al riguardo, almeno per quanto riguarda il contesto di questo libro. È proprio questo uno
dei motivi per cui questo libro è stato scritto: per portare altri a comprendere la natura
insidiosa della sicurezza e come sia importante affrontarla nella maniera corretta.
Un altro motivo importante per implementare correttamente almeno la crittografia
è stato già accennato in precedenza: gli attacchi alla crittografia sono particolarmente
dannosi perché possono essere invisibili. Se l’attaccante riesce a compromettere il vostro
sistema crittografico, verosimilmente non ve ne accorgerete. È come un ladro che ha
una serie di chiavi per entrare in casa vostra: se presterà sufficiente attenzione, quando
mai potrete accorgervi che sia entrato?
Il nostro obiettivo a lungo termine è quello di realizzare sistemi informatici sicuri. Per
raggiungere questo obiettivo, ognuno deve fare la propria parte. Questo libro tratta i
metodi per realizzare una crittografia sicura. Anche le atre parti del sistema dovranno
essere rese sicure. La sicurezza globale del sistema verrà limitata dall’elemento più debole,
e faremo il possibile perché questo non sia mai rappresentato dalla crittografia.
Un altro motivo importante per realizzare una buona crittografia è che i sistemi crittografici, una volta che sono stati implementati, sono molto difficili da modificare. Un
sistema operativo viene eseguito su un singolo computer, ma i sistemi crittografici spesso
vengono utilizzati nei protocolli di comunicazione per consentire a più computer di
comunicare tra loro.Aggiornare il sistema operativo di un computer è fattibile, e in realtà
viene fatto piuttosto spesso. Modificare i protocolli di comunicazione in una rete è un
incubo, e per questo molte reti utilizzano ancora progetti degli anni ’70 e ’80. Ricordate
che qualsiasi nuovo sistema crittografico progettiate oggi, se adottato su ampia scala, sarà
probabilmente ancora in uso fra 30 o 50 anni. Speriamo che per allora le altre parti del
sistema avranno raggiunto un livello di sicurezza molto più alto, e di sicuro non vogliamo
che la crittografia costituisca l’elemento debole.

8.1 Creazione di programmi corretti
Il nocciolo del problema dell’implementazione è che nel settore dell’informatica non
si sa come scrivere un programma o un modulo corretto (un programma “corretto” è
quello che si comporta esattamente secondo le sue specifiche). I motivi alla base di questa
apparente difficoltà nello scrivere programmi corretti sono molti.

Aspetti relativi all’implementazione (I)   109

8.1.1 Le specifiche
Il primo problema è che per la gran parte dei programmi non esiste una descrizione
chiara di ciò che dovrebbero fare. Se non ci sono specifiche, non è neppure possibile
verificare la correttezza di un programma. Per programmi di questo tipo l’intero concetto
di correttezza rimane indefinito.
Molti progetti software prevedono un documento denominato specifica funzionale. In
teoria questa dovrebbe essere la specifica del programma. Nella realtà, però, questo
documento spesso non esiste, è incompleto o specifica elementi irrilevanti per il comportamento del programma. Senza specifiche chiare non c’è speranza di ottenere un
programma corretto.
La definizione delle specifiche in realtà è un processo a tre stadi.
• Requisiti. I requisiti sono una descrizione informale di ciò che si pensa che il
programma dovrebbe ottenere. È sostanzialmente un documento che indica “che
cosa può fare” piuttosto che “come utilizzarlo”. I requisiti spesso sono relativamente
vaghi e tralasciano molti dettagli per concentrarsi su un quadro più ampio.
• Specifica funzionale. La specifica funzionale offre una definizione dettagliata e
completa del comportamento del programma. Può contenere solamente elementi
misurabili dall’esterno del programma.
Per ciascun elemento della specifica funzionale, chiedetevi se a programma concluso
potreste essere in grado di realizzare un test per determinare se questa elemento
sia stato rispettato o meno. Il test può basarsi solo sul comportamento esterno
del programma, e su nessun elemento interno. Se non è possibile creare un test
per un determinato elemento, allora questo non può trovare posto nella specifica
funzionale.
La specifica funzionale dovrebbe essere completa. Ogni elemento delle funzionalità
dovrebbe essere specificato. Tutto ciò che non compare nella specifica funzionale
non deve essere implementato.
Un altro modo per pensare alla specifica funzionale è vederla come la base per il test
del programma completato. Ogni elemento può, e dovrebbe, essere testato.
• Progetto di implementazione. Questo documento viene chiamato in molti
modi, ma comunque specifica il funzionamento interno del programma. Contiene
tutti gli elementi che non possono essere testati dall’esterno. Un buon progetto di
implementazione spesso suddivide il programma in diversi moduli e descrive le
funzionalità di ognuno di essi. A loro volta, le descrizioni dei moduli possono essere
viste come i requisiti dei moduli, facendo ripartire l’intero ciclo, ma questa volta
suddividendo ogni modulo in vari sottomoduli.
Dei tre documenti, il più importante è certamente la specifica funzionale. È il documento rispetto al quale il programma sarà testato una volta completato. A volte è possibile
cavarsela con requisiti informali, o con un progetto di implementazione che si riduce a
qualche schizzo su una lavagna, ma senza specifiche funzionali non c’è modo di descrivere
quanto è stato raggiunto a programma completato.

110   Capitolo 8

8.1.2 Test e correzioni
Il secondo problema, nella scrittura di programmi corretti, è rappresentato dal metodo
di sviluppo basato su test e correzioni, ormai utilizzato quasi universalmente. I programmatori scrivono un programma e poi ne verificano il corretto funzionamento. In caso
di problemi, correggono i bug e ripetono i test. Come è noto, questo metodo non porta
a ottenere un programma corretto, ma semplicemente a un programma che “funziona”
nelle situazioni più comuni.
Nel 1972 Edsger Dijkstra, in occasione della conferenza per il Turing Award, disse che i
test possono solamente evidenziare la presenza di bug, mai la loro assenza [35]. Questo è
vero, e idealmente vorremmo poter scrivere programmi ed essere in grado di dimostrare che sono corretti. Sfortunatamente le tecniche attuali per provare la correttezza dei
programmi non sono abbastanza buone per gestire le singole attività di programmazione
quotidiana, e men che meno un intero progetto.
Gli informatici non sanno come risolvere questo problema. Forse in futuro si riuscirà
a dimostrare quando un programma è corretto. Forse servono solo un’infrastruttura e
una metodologia di test molto più complete e approfondite. Ma anche senza avere a
disposizione questa soluzione completa, possiamo comunque fare del nostro meglio con
gli strumenti che abbiamo già a disposizione.
Esistono alcune semplici regole sui bug che ogni buon libro di ingegneria del software
non trascura.
• Quando si trova un bug, occorre innanzitutto implementare un test in grado di rilevarlo. Quindi occorre verificare che il bug venga rilevato correttamente. Poi si può
passare alla correzione del problema e alla verifica che non venga più individuato dal
test. Il test poi dovrà essere eseguito con ogni versione successiva, per essere sicuri
che il problema non si ripresenti.
• Quando si trova un bug, occorre pensare a che cosa possa averlo provocato. Ci sono
altre posizioni in cui potrebbe nascondersi? Vanno controllate tutte.
• Occorre tenere traccia di tutti i problemi individuati. Una semplice analisi statistica
dei problemi riscontrati è in grado di evidenziare la parte del programma con il
maggior numero di errori, o anche il tipo di errore commesso con maggiore frequenza. Questo feedback è necessario per un sistema di controllo della qualità.
Queste regole rappresentano soltanto il livello minimo indispensabile, ma non esiste
una metodologia definita a cui appoggiarsi. I libri che trattano di qualità del software
non sono molti, e tra di essi non c’è pieno accordo. Molti indicano una determinata
metodologia di sviluppo del software come la soluzione ideale, ma sospettiamo sempre
di simili schemi che si propongono come la cura per tutti i mali. La verità si trova quasi
sempre nel mezzo.

8.1.3 Atteggiamento tollerante
Il terzo problema è rappresentato dall’atteggiamento decisamente tollerante assunto da
molti nel settore informatico. La presenza di errori nei programmi viene spesso accettata
come un dato di fatto. Se un elaboratore di testi si blocca e manda in fumo una giornata di lavoro, tutti sembrano pensare che sia un fatto piuttosto normale e accettabile.
Spesso si attribuisce la colpa all’utente: “Avresti dovuto salvare il lavoro più spesso”. Le

Aspetti relativi all’implementazione (I)   111

aziende del settore distribuiscono periodicamente prodotti che presentano bug noti.
Non sarebbe poi così grave se si trattasse soltanto di giochi, ma oggigiorno il nostro
lavoro, la nostra economia e – sempre di più – la nostra vita dipendono dal software.
Se un’azienda automobilistica riscontra un difetto (un bug) in un’auto dopo la vendita,
esegue un richiamo e il problema verrà risolto. Le aziende che producono software se la
cavano spesso declinando qualsiasi responsabilità nell’accordo di licenza del programma,
cosa che non potrebbero fare se producessero qualsiasi altro bene. Questo atteggiamento
tollerante e lassista indica che non si stanno ancora facendo sforzi abbastanza seri per
produrre software corretto.

8.1.4 Come procedere?
Non pensate che tutto ciò di cui avete bisogno siano un buon programmatore, buone
revisioni del codice o anche un processo di sviluppo certificato ISO 9001 o test approfonditi, o perfino una combinazione di tutti questi elementi. La realtà e molto più
difficile. Il software è un’entità troppo complessa per poter essere addomesticata con
qualche regola e qualche procedura. Troviamo istruttivo guardare al migliore sistema di
controllo della progettazione al mondo: quello dell’industria aeronautica. Ogni addetto
del settore è coinvolto nella sicurezza. Esistono regole molto rigide per quasi tutte le
operazioni. Sono presenti backup multipli in caso di guasto. Ogni vite e ogni bullone di
un aereo devono essere dichiarati idonei all’utilizzo per il volo, prima di potere essere
usati. Quando un meccanico usa un cacciavite sull’aereo, il suo lavoro viene controllato
e attestato da un supervisore. Ogni modifica viene registrata con precisione. Qualsiasi
incidente viene indagato a fondo per individuarne ogni possibile causa, che poi viene
corretta. Questa maniacale ricerca della qualità ha costi molto alti. Un aereo costerebbe
probabilmente meno, di ordini di grandezza enormi, se ci si limitasse a inviare i disegni
a una qualsiasi azienda meccanica. Ma la ricerca della qualità si è anche rivelata incredibilmente efficace. Volare oggi è diventata una routine, in una macchina per cui ogni
problema può avere effetti potenzialmente fatali – una macchina per cui, se qualcosa va
storto, non basta tirare i freni e fermarsi. Un apparecchio per il quale l’unica possibilità
di tornare intatto al suolo è la delicata operazione di atterraggio su uno dei pochi luoghi
nel mondo preparati a questo scopo in maniera particolare. L’industria aeronautica è
stata incredibilmente efficiente nel rendere sicuro il volo. Forse scrivere software corretto
finirebbe per costare un ordine di grandezza in più di quanto siamo abituati a vedere
oggi. Ma dato il prezzo che la società deve pagare per i bug nel software, siamo sicuri
che nel lungo termine il tutto si rivelerebbe redditizio.

8.2 Creazione di software sicuro
Finora abbiamo parlato solamente di software corretto, ma scrivere software corretto non
è sufficiente per un sistema di sicurezza. Il software deve anche essere sicuro.
Qual è la differenza? Il software corretto ha una funzionalità specifica. Se si preme il
pulsante A, si otterrà il comportamento B. Il software sicuro ha un requisito ulteriore:
una mancanza di funzionalità. A prescindere da ciò che un attaccante fa, non sarà mai
in grado di fare X. Questa è una differenza decisamente fondamentale: si può testare
una funzionalità, ma non l’assenza di una funzionalità. Non esiste un modo efficiente

112   Capitolo 8

per testare gli aspetti di sicurezza di un software, e questo rende la scrittura software
sicuro un’attività molto più complessa del realizzare software corretto. La conclusione
inevitabile è che:
Le tecniche di implementazione standard
sono totalmente inadatte alla creazione di codice sicuro.
Al momento non sappiamo come creare codice sicuro. La qualità del software copre
un ambito decisamente ampio, e la sua trattazione richiederebbe svariati volumi. Non
abbiamo le conoscenze sufficienti per scrivere questi volumi, ma conosciamo bene le
questioni legate alla crittografia e i problemi che incontriamo più spesso, e questi sono
gli argomenti di cui parleremo nel resto del capitolo.
Prima di cominciare, vogliamo chiarire il nostro punto di vista: se non siete seriamente
disposti a impegnarvi per sviluppare un’implementazione sicura, non ha molto senso che
vi preoccupiate della crittografia. Progettare sistemi crittografici può essere divertente,
ma in genere la crittografia non è che una piccola parte di un sistema più ampio.

8.3 Mantenere i segreti
Quando si lavora con la crittografia, si ha che fare con i segreti. E i segreti devono rimanere tali. Questo significa che il software che si occupa dei segreti deve assicurare che
essi non possano trapelare.
Per il canale sicuro abbiamo due tipi di segreti: le chiavi e i dati. Sono entrambi segreti
transitori; non occorre che siano conservati a lungo. I dati vengono memorizzati solo per il
tempo di elaborazione di ogni messaggio. Qui parleremo solo di come mantenere i segreti
transitori. Di come trattare quelli a lungo termine ci occuperemo nel Capitolo 21.
I segreti transitori sono mantenuti nella memoria, e sfortunatamente la memoria nella
gran parte dei computer non è molto sicura. Esamineremo separatamente ognuno dei
problemi più tipici.

8.3.1 Cancellazione dello stato
Una regola fondamentale per scrivere software per la sicurezza: eliminate qualsiasi informazione appena non serve più. Più a lungo la mantenete, maggiore è la possibilità che
qualcuno possa accedervi. Di più, dovreste eliminare i dati in maniera definitiva prima di
rischiare di perdere il controllo sul supporto di memorizzazione sottostante. Per i segreti
transitori, questo comporta la cancellazione delle aree di memoria.
Sembra facile, ma questa attività porta a un numero sorprendente di problemi. Se scrivete
tutto il programma in C, potete occuparvi da soli della cancellazione. Se scrivete una
libreria che verrà utilizzata da altri, dovrete fare affidamento sul fatto che il programma
principale vi informi che lo stato non serve più. Per esempio, quando la connessione
di una comunicazione viene interrotta, la libreria crittografica dovrebbe venire informata, in modo che possa cancellare lo stato di sessione del canale sicuro. La libreria può
contenere una funzione dedicata a questo scopo, ma è verosimile che il programmatore
dell’applicazione non la richiamerà. Dopo tutto, il programma funziona perfettamente
anche senza richiamare tale funzione.

Aspetti relativi all’implementazione (I)   113

In alcuni linguaggi orientati agli oggetti le cose sono un poco più semplici. In C++
esiste una funzione distruttore per ogni oggetto, ed essa è in grado di azzerare lo stato.
Questa è sicuramente una pratica standard per il codice legato alla sicurezza scritto in
C++. Finché il programma principale si comporta correttamente e distrugge tutti gli
oggetti di cui non ha più bisogno, lo stato della memoria verrà cancellato correttamente. Il
linguaggio C++ si assicura che tutti gli oggetti allocati sullo stack vengano correttamente
distrutti quando lo stack viene svuotato nella gestione delle eccezioni, ma il programma
deve assicurarsi che siano distrutti tutti gli oggetti allocati nello heap. La chiamata di una
funzione del sistema operativo per uscire dal programma potrebbe anche non svuotare lo
stack delle chiamate. E occorre assicurarsi che tutti i dati sensibili siano eliminati anche
se il programma sta per terminare. Dopo tutto, il sistema operativo non offre garanzie sul
fatto che i dati verranno eliminati in fretta, e alcuni sistemi operativi non si preoccupano
nemmeno di ripulire la memoria prima di assegnarla a un’altra applicazione.
Anche se svolgeste tutte queste operazioni, il computer potrebbe ancora frustrare i vostri
sforzi. Alcuni compilatori cercano di ottimizzare eccessivamente. Una tipica funzione di
sicurezza svolge alcuni calcoli in variabili locali, poi cerca di azzerarle. In C lo scopo può
essere raggiunto con una chiamata della funzione memset. I buoni compilatori riusciranno
a ottimizzare la funzione ottenendo codice inline, una modalità più efficiente. Alcuni
invece fanno troppo i furbi: individuano che la variabile o l’array che si sta azzerando
non verranno più utilizzati e così “ottimizzano” eliminando la chiamata di memset. La
velocità è maggiore, ma all’improvviso il programma non si comporta più come prima.
Non è raro vedere del codice che rivela i dati che trova in memoria. Se la memoria viene
assegnata a una libreria senza prima essere stata azzerata, la libreria potrebbe lasciar filtrare
i dati a un attaccante. Pertanto, controllate il codice prodotto dal vostro compilatore e
accertatevi che i segreti vengano effettivamente cancellati.
In un linguaggio come Java, la situazione è ancora più complicata.Tutti gli oggetti vivono
sullo heap, e su questo viene svolta la garbage collection. Ciò significa che la funzione di
finalizzazione (simile al distruttore del linguaggio C++) non viene richiamata finché il
garbage collector non realizza che l’oggetto non è più utilizzato. Non esistono specifiche
sulla frequenza di esecuzione del garbage collector, ed è piuttosto verosimile che dati
segreti possano rimanere in memoria per molto tempo. L’utilizzo della gestione delle
eccezioni rende complicato svolgere l’azzeramento manualmente. Se viene sollevata
un’eccezione, allora lo stack delle chiamate si svuota senza che il programmatore possa
inserire il proprio codice, a meno di non scrivere ogni funzione come una grande clausola try. L’ultima soluzione è talmente orrenda da non essere realistica; inoltre dovrebbe
essere applicata per tutto il programma, rendendo impossibile la creazione di una libreria
di sicurezza per Java che si comporti correttamente. Durante la gestione delle eccezioni,
Java svuota allegramente lo stack, gettando via i riferimenti agli oggetti senza ripulire gli
oggetti stessi. Java è veramente pessimo a questo riguardo. La soluzione migliore che siamo
riusciti a trovare consiste nell’assicurare almeno che le routine di finalizzazione vengano
eseguite all’uscita dal programma. Il metodo main del programma usa una dichiarazione
try-finally. Il blocco finally contiene del codice che forza una garbage collection e
istruisce il garbage collector per fare in modo che cerchi di completare tutti i metodi
di finalizzazione (per maggiori dettagli fate riferimento alle funzioni System.gc() e
System.runFinalization()). Non c’è ancora alcuna garanzia che i metodi di finalizzazione
verranno eseguiti, ma questo è il meglio che siamo riusciti a trovare.

Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

114   Capitolo 8

Ciò di cui abbiamo veramente bisogno è il supporto dello stesso linguaggio di programmazione. In C++ è almeno teoricamente possibile scrivere un programma che azzeri
tutti gli stati non appena non servono più, ma molte altre caratteristiche del linguaggio
ne fanno una pessima scelta per un software di sicurezza. Java rende molto difficile azzerare lo stato. Un miglioramento potrebbe essere quello di dichiarare le variabili come
“sensibili” e fare in modo che l’implementazione ne garantisca l’azzeramento. Ancora
migliore sarebbe un linguaggio che elimina i dati non più necessari. Questo eviterebbe
una grande quantità di errori senza avere impatti significativi sull’efficienza.
Ci sono altre posizioni in cui i dati segreti possono finire. Tutti i dati alla fine vengono
caricati in un registro della CPU. Azzerare i registri è un compito impossibile nella
maggior parte dei linguaggi di programmazione, ma in CPU povere di registri come
quelle della serie x86, è molto improbabile che qualsiasi dato possa sopravvivere per un
periodo di tempo ragionevole. Durante un cambio di contesto, o context-switch (quando
il sistema operativo passa dall’esecuzione di un programma a quella di un altro), i valori
dei registri della CPU vengono salvati in memoria, dove potrebbero rimanere a lungo.
Per quanto sappiamo, non c’è nulla che si possa fare a questo riguardo, a parte correggere
il sistema operativo in modo che possa garantire la riservatezza dei dati.

8.3.2 File di swapping
La maggior parte dei sistemi operativi (comprese tutte le versioni odierne di Windows e
tutte le versioni di UNIX) utilizza un meccanismo di memoria virtuale per aumentare il
numero dei programmi che possono essere eseguiti in parallelo. Quando un programma
è in esecuzione, non tutti i suoi dati vengono mantenuti in memoria; alcuni vengono
memorizzati in un file di swapping, o di paginazione. Quando il programma tenta di accedere
a dati che non si trovano in memoria, viene interrotto. Il sistema di memoria virtuale
legge i dati dal file di swapping e li carica in una porzione di memoria, e il programma
torna così in grado di continuare. Inoltre, quando il sistema di memoria virtuale decide
che serve ulteriore memoria libera, prende una porzione arbitraria di memoria da un
programma e la scrive nel file di swapping.
Ovviamente non tutti i sistemi e le configurazioni di memoria virtuale mantengono
la segretezza dei dati o li cifrano prima della scrittura su disco. Il software in genere è
progettato per un ambiente cooperativo, non per l’ambiente ostile in cui operano i
crittografi. Il nostro problema, quindi, diventa questo: il sistema di memoria virtuale
potrebbe semplicemente prendere una parte della memoria del nostro programma e
scriverla su disco nel file di swapping. Il programma non viene mai informato della faccenda, e non se ne accorge. Supponete che questo capiti alla memoria nella quale sono
salvate le chiavi. Se il computer si blocca, o viene spento, i dati rimangono sul disco. La
maggior parte dei sistemi operativi lascia i dati sul disco anche quando viene effettuato
uno spegnimento regolare. Potrebbe non esservi un meccanismo che azzeri il file di
swapping, per cui i dati potrebbero rimanere sul disco per un tempo indefinito. Chi può
sapere chi avrà accesso a questo file di paginazione in futuro? Non possiamo davvero
permetterci il rischio che i nostri segreti siano salvati nel file di paginazione (in effetti,
non dovremmo mai scrivere dati segreti su qualsiasi supporto permanente senza cifrarli,
ma questo è un argomento che tratteremo più avanti).
Come possiamo impedire che il sistema di memoria virtuale scriva i nostri dati su disco?
In alcuni sistemi operativi è possibile usare delle chiamate di sistema per informare che

Aspetti relativi all’implementazione (I)   115

porzioni specifiche della memoria non devono mai essere inviate al file di paginazione.
Alcuni sistemi operativi supportano un sistema di swapping sicuro in cui i dati inviati
al file di paginazione vengono protetti crittograficamente, ma questi sistemi potrebbero
richiedere all’utente di attivare i flag di configurazione appropriati. Se nessuna di queste
opzioni è disponibile su tutti i sistemi che dovranno eseguire la vostra applicazione, potreste non essere in grado di fare nulla per proteggervi da questa particolare possibilità
di attacco.
Presumendo che siate in grado di bloccare la memoria e impedire che venga spostata
nel file di swapping, quale memoria dovreste bloccare? Tutta la memoria che potrà mai
contenere dei segreti, ovviamente. Questo introduce un altro problema. In molti ambienti di programmazione è assai complicato conoscere la posizione esatta in cui i dati
vengono memorizzati. Gli oggetti spesso vengono allocati in uno heap, i dati possono
essere allocati in maniera statica, e molte variabili locali finiscono sullo stack. Capire i
dettagli è un’attività complicata e molto soggetta a errori. Probabilmente la soluzione
migliore è semplicemente quella di bloccare tutta la memoria della vostra applicazione.
E anche questo non é facile come sembra, dato che potrebbe causare la perdita di molti
servizi del sistema operativo, come l’allocazione automatica dello stack. E bloccando
tutta la memoria si rende inefficace il sistema di memoria virtuale.
Non dovrebbe essere così difficile. La soluzione adatta, ovviamente, è quella di realizzare
un sistema di memoria virtuale che protegga la riservatezza dei dati. Ciò richiederebbe
una modifica al sistema operativo, ed è fuori dal nostro controllo. Anche se la prossima
versione del vostro sistema operativo dovesse avere questa caratteristica, dovreste controllare con attenzione che il sistema di memoria virtuale svolga al meglio il compito di
mantenere i segreti. E, in base alla vostra applicazione, potreste ancora trovarvi a dover
gestire il fatto che questa deve potere essere eseguita su sistemi più vecchi o su sistemi
con configurazioni insicure.

8.3.3 Memoria cache
I computer moderni non hanno un solo tipo di memoria, ma una gerarchia di memorie.
Alla base c’è la memoria principale, spesso con dimensioni di vari gigabyte. Ma poiché
la memoria principale è relativamente lenta, esiste anche una memoria cache. Si tratta di
una memoria di dimensioni più ridotte, ma più veloce. La cache mantiene una copia
dei dati della memoria principale utilizzati più di recente. Se la CPU vuole accedere ai
dati, prima controlla nella cache. Se i dati si trovano nella cache, la CPU li recupera in
maniera relativamente veloce; in caso contrario, i dati vengono letti (in maniera relativamente lenta) dalla memoria principale, e una copia viene memorizzata nella cache
per eventuali utilizzi futuri. Per fare posto nella cache, a ogni nuova entrata, la copia di
qualche altra porzione di dati viene eliminata.
Questo fatto è importante, perché la cache mantiene copie dei dati, incluse copie dei
nostri dati segreti. Il problema è che, quando cerchiamo di eliminare i nostri segreti,
questa eliminazione potrebbe non avvenire nella maniera corretta. In alcuni sistemi le
modifiche sono scritte solo nella cache e non nella memoria principale. I dati alla fine
verranno scritti nella memoria principale, ma solo quando la cache avrà bisogno di più
spazio per ospitare altri dati. Noi non conosciamo i dettagli di questi sistemi, ed essi
cambiano al cambiare della CPU. Non c’è modo di sapere se esista qualche interazione
tra l’unità di allocazione della memoria e il sistema della cache che possa far sì che qual-

116   Capitolo 8

che operazione di azzeramento sfugga alla parte di scrittura nella memoria principale,
quando la memoria viene deallocata prima della cancellazione della cache. I produttori
non specificano mai un modo per cancellare la memoria in un modo certo. O almeno,
non abbiamo mai visto specifiche di questo tipo, e finché non esiste una specifica, non
ci possiamo fidare.
Un pericolo della cache è che, in determinate situazioni, essa viene a sapere che una
particolare area di memoria è stata modificata, magari da un’altra CPU in un sistema a
più CPU. La cache a questo punto contrassegna i dati che contiene per quella locazione
di memoria come “invalidi”, ma in genere i dati non vengono realmente azzerati.Ancora
una volta, potrebbe esistere una copia dei nostri segreti che non è stata azzerata.
Non si può davvero fare molto a questo riguardo. Non si tratta di un pericolo molto
grande, perché nella gran parte dei sistemi, a meno di attacchi fisici, solo il codice del
sistema operativo può accedere in maniera diretta al meccanismo della cache. E comunque ci dobbiamo fidare del sistema operativo, per cui potremmo fidarci anche in
questo caso. Tuttavia siamo preoccupati di questo tipo di progettazioni, perché è chiaro
che non offrono le funzionalità richieste per implementare un sistema di sicurezza nella
maniera corretta.

8.3.4 Mantenimento dei dati in memoria
Molti si stupiscono del fatto che, quando si sovrascrivono dei dati in memoria, questi
non vengono cancellati. I dettagli dipendono fino a un certo punto dall’esatto tipo di
memoria coinvolto, ma fondamentalmente, se si salvano dei dati in un’area di memoria,
quest’area inizia lentamente a “imparare” i dati. Quando si effettua una sovrascrittura o si
spegne il computer, i vecchi valori non vanno del tutto perduti. In base alle circostanze,
limitandosi a spegnere e riaccendere la memoria si possono recuperare alcuni o anche
tutti i vecchi dati. Altre memorie possono “ricordare” vecchi dati se vi si accede usando
modalità di test (spesso non documentate) [57].
I meccanismi che causano questo fenomeno sono diversi. Se gli stessi dati rimangono
memorizzati per un certo tempo nella stessa posizione della SRAM (RAM statica), finiranno per diventare lo stato di accensione preferenziale di tale memoria. Un nostro amico
ebbe questo problema molto tempo fa con il suo computer realizzato in casa [17]. Aveva
scritto un BIOS che usava un valore magico in una particolare locazione di memoria per
determinare se un reset fosse stato causato da un riavvio a caldo o a freddo. A quel tempo
le macchine realizzate in casa venivano programmate inserendo direttamente la forma
binaria del linguaggio macchina. Questo provocava molti errori, e l’unico modo sicuro
di recuperare da un programma andato in crash era il reset della macchina. Un riavvio
a freddo è quello che si ha dopo l’accensione. Un riavvio a caldo è quello che si ottiene
quando si preme il pulsante reset. Un riavvio a caldo non reinizializza completamente lo
stato, e pertanto non azzera le impostazioni effettuate dall’utente. Dopo qualche tempo
la macchina del nostro amico iniziò a rifiutare di avviarsi dopo l’accensione, perché la
memoria aveva imparato il valore magico, e il processo di avvio aveva iniziato a trattare
ogni reset come un riavvio a caldo. Dato che per questo motivo le variabili corrette non
venivano inizializzate, il processo di avvio si concludeva con un fallimento. La soluzione
in questo caso fu quella di scambiare di posto alcuni chip di memoria, scompaginando
il numero magico che la SRAM aveva imparato. Per noi fu una lezione da ricordare: la
memoria ricorda più dati di quanto si possa pensare.

Aspetti relativi all’implementazione (I)   117

Processi simili avvengono nella DRAM (RAM dinamica), anche se sono in certa
misura più complessi. La DRAM funziona immagazzinando una piccola carica in un
piccolissimo condensatore. Il materiale isolante che circonda il condensatore subisce
una tensione dal campo risultante. Tale tensione si ripercuote sul materiale, provocando
una migrazione delle impurità [57]. Un attaccante che abbia un controllo fisico sulla
memoria è potenzialmente in grado di ottenere questi dati. Inoltre, dato il modo in cui
i condensatori della DRAM si scaricano, il loro valore potrebbe rimanere intatto per
alcuni secondi a temperatura ambiente quando viene tolta la corrente, o anche più a
lungo se la memoria viene raffreddata.
Questi sono problemi importanti. L’ultima categoria è stata recentemente oggetto di
illustrazione nel contesto di alcuni attacchi di avvio a freddo [59]. I ricercatori sono stati
in grado di ricavare chiavi crittografiche segrete dalle memorie dei computer dopo il
riavvio. È stato possibile anche estrarre fisicamente la memoria da un computer, inserirla
in un altro e ricavare le chiavi crittografiche. Se il vostro computer venisse compromesso (per esempio rubato), non vorreste certo che lo fossero anche i dati che avevate in
memoria. Per raggiungere lo scopo occorre fare in modo che il computer dimentichi
le informazioni.
Possiamo fornire soltanto una soluzione parziale, che funziona se partiamo da alcuni
presupposti ragionevoli sulla memoria. La soluzione, che chiameremo Boojum (dall’opera di Lewis Carroll The Hunting of the Snark [24]), funziona con quantità relativamente
piccole di dati, come le chiavi. La nostra descrizione del Boojum è stata leggermente
aggiornata rispetto alla prima edizione di questo libro e comprende una difesa dall’attacco dell’avvio a freddo [59]. Per il Boojum, sia m il dato che vogliamo memorizzare.
Anziché salvare m, generiamo una stringa casuale R e memorizziamo sia R che h(R) ⊕ m
dove h è una funzione di hash. Questi due valori sono salvati in locazioni di memoria
differenti, preferibilmente non troppo vicine. Un trucco è quello di cambiare R con
regolarità.A intervalli regolari, per esempio ogni secondo, generiamo un nuovo R’ casuale
e aggiorniamo la memoria in modo che salvi R ⊕ R’ e h(R ⊕ R’) ⊕ m. In questo modo
ci assicuriamo che ogni porzione della memoria venga scritta con una sequenza di bit
casuali. Per azzerare la memoria è sufficiente scrivere un nuovo m con il valore zero.
Per ricavare informazioni da questa memoria, si devono leggere entrambe le parti, eseguire
una funzione di hash sulla prima, e farne l’xor per estrarre m. La scrittura si effettua
facendo l’xor dei nuovi dati con h(R) e salvando nella seconda locazione.
Si deve prestare attenzione a che i bit di R e h(R) ⊕ m non siano vicini sul chip della
RAM. Senza informazioni sulle modalità di funzionamento di questo chip, questo può
essere difficile da ottenere, ma la maggior parte delle memorie salva i bit in una matrice
rettangolare, con alcuni bit di indirizzo che identificano le righe e altri che identificano
le colonne. Se le due parti sono salvate in indirizzi che differiscono per 0x5555, è molto
improbabile che verranno scritte vicine sul chip (ciò presume che la memoria non usi
i bit di indirizzo pari come numeri di riga e i dispari come numeri di colonna, ma non
abbiamo mai visto una progettazione di questo tipo). Una soluzione ancora migliore
potrebbe essere quella di scegliere due indirizzi a caso in uno spazio di indirizzamento
molto ampio. Questo riduce notevolmente la probabilità che due locazioni siano vicine,
indipendentemente dall’effettiva disposizione dei chip della memoria.
Questa è solo una soluzione parziale, e anche piuttosto complessa. È limitata a piccole
quantità di dati, ma può assicurare che non ci sia un punto fisico del chip di memoria
che verrà continuamente messo sotto tensione o meno in base ai dati segreti. Inoltre,

118   Capitolo 8

fintanto che k bit di R rimangono non recuperabili, l’attaccante dovrebbe svolgere una
ricerca esaustiva per questi k prima di poter ricostruire h(R) ⊕ m.
Non c’è ancora alcuna garanzia che la memoria verrà azzerata. Se leggete la documentazione di un chip di memoria, non troverete specifiche che impediscano al chip di
mantenere tutti i dati che vi vengono salvati. Nessun chip fa questo, ovviamente, e ciò
fa capire che possiamo raggiungere al massimo una sicurezza euristica.
Ci siamo concentrati sulla memoria principale. La stessa soluzione funzionerà anche per
la memoria cache, tranne per il fatto che non sarà possibile controllare la posizione del
chip in cui i dati verranno salvati. Questa soluzione non funziona per i registri della CPU,
ma essi sono usati talmente spesso e per così tanti dati diversi che dubitiamo possano
causare un problema di mantenimento dei dati. D’altra parte, i registri estesi, come i
registri a virgola mobile o i registri di tipo MMX, vengono utilizzati con una frequenza
molto inferiore, per cui potrebbero rappresentare un problema.
Se si devono mantenere segrete grandi quantità di dati, la soluzione che prevede di salvare due copie e fare regolarmente l’xor di nuove stringhe casuali in entrambe le copie
diventa troppo onerosa. Una scelta migliore è quella di cifrare un grosso blocco di dati
e salvare il testo cifrato in una memoria che potenzialmente mantiene l’informazione.
Solo la chiave deve essere salvata in un modo che impedisca la ritenzione dei dati, per
esempio con un Boojum. Per i dettagli, cfr. [32].

8.3.5 Accessi indesiderati
C’è un altro problema nel mantenere segreti i dati su un computer: altri programmi sulla
stessa macchina potrebbero accedere agli stessi dati. Alcuni sistemi operativi permettono a programmi diversi di condividere la memoria. Se l’altro programma è in grado di
leggere le vostre chiavi segrete, avete un grosso problema. Spesso la memoria condivisa
deve essere impostata da entrambi i programmi, e questo riduce i rischi. In altre situazioni, la memoria condivisa potrebbe essere impostata in automatico come risultato del
caricamento di una libreria condivisa.
I programmi di debugging sono particolarmente pericolosi. I moderni sistemi operativi spesso contengono caratteristiche progettate per essere utilizzate dai programmi
di debugging. Diverse versioni di Windows consentono di collegare un programma di
debugging a un processo già in esecuzione. Il programma può fare molte cose, tra cui
leggere la memoria. In UNIX a volte è possibile forzare un core dump di un programma.
Il core dump è un file con un’immagine della memoria contenente i dati del programma,
compresi eventuali segreti.
Un altro pericolo proviene da utenti con permessi speciali. Come superuser o amministratori, questi utenti possono accedere a parti della macchina precluse agli utenti normali.
In UNIX, per esempio, il superuser può leggere qualsiasi parte della memoria.
In generale, un programma non può difendersi in maniera efficace da questi tipi di attacchi. Prestando molta attenzione potreste riuscire a eliminare alcuni di questi problemi,
ma spesso le vostre possibilità di azione saranno limitate.Tuttavia, è utile tenere conto di
questi aspetti per la particolare piattaforma sulla quale lavorate.

Aspetti relativi all’implementazione (I)   119

8.3.6 Integrità dei dati
Oltre a mantenere i segreti, dovremmo proteggere l’integrità dei dati che stiamo salvando.
Per proteggere i dati in transito usiamo il MAC, ma se i dati possono essere modificati
mentre si trovano nella memoria, il problema rimane.
Per questa discussione partiremo dal presupposto che l’hardware sia affidabile. Se così non
fosse, non ci sarebbe davvero molto da fare. Se non siete sicuri del grado di affidabilità
dell’hardware, forse dovreste spendere parte del vostro tempo e della memoria per una
verifica, anche se in realtà questo è il compito del sistema operativo.
Una cosa che possiamo fare è assicurarci che la memoria principale delle nostre macchine
sia del tipo ECC (Error-Correcting Code). Occorre accertarsi che tutti i componenti del
computer supportino la memoria ECC. Non fidatevi dei moduli di memoria meno costosi
che non memorizzano i dati aggiuntivi, ma li ricalcolano al volo; questo metodo vanifica
lo scopo della memoria ECC. Se si verifica un problema con un singolo bit, questo tipo
di memoria è in grado di individuarlo e correggerlo. Senza la memoria ECC, qualsiasi
errore di un bit porta a una lettura errata dei dati da parte della CPU.
Perché tutto ciò è importante? In un computer moderno c’è un numero enorme di bit.
Supponete che la parte ingegneristica sia stata svolta molto bene, e che ogni bit abbia solo
10−15 possibilità al secondo di presentare un problema. Con 128 MB di memoria i bit
sono circa 109, e ci si può aspettare un problema a un bit ogni 11giorni. La percentuale
di errori aumenta con l’aumentare della memoria della macchina, perciò è ancora peggio
se si ha 1 GB di memoria, con un problema ogni 32 ore. I server tipicamente utilizzano
moduli ECC perché hanno maggiori quantità di memoria e rimangono attivi per molto
più tempo. Ci piacerebbe avere la stessa stabilità in tutte le macchine.
Naturalmente questa è una questione che riguarda l’hardware, e in genere non si arriva
a specificare il tipo di memoria richiesto per la macchina che dovrà eseguire un’applicazione.
Alcuni dei pericoli che minacciano la riservatezza dei dati ne mettono in pericolo anche l’integrità. I programmi di debugging a volte possono modificare la memoria di un
programma.Anche i superuser possono modificare la memoria in maniera diretta.Anche
qui non si può fare nulla, ma è bene essere consapevoli di come stanno le cose.

8.3.7 Che cosa fare
Mantenere un segreto su un computer moderno non è semplice come sembra. Il segreto
potrebbe venire rivelato in molti modi. Per essere davvero efficienti, dovete impedirli
tutti. Sfortunatamente, i sistemi operativi e i linguaggi di programmazione di oggi non
offrono il supporto necessario per arrestare in maniera definitiva le perdite di dati. Si
deve fare il possibile, e questo comporta un grande lavoro, svolto in base all’ambiente
in cui ci si trova a operare.
Questi problemi rendono molto difficile creare una libreria che contenga funzioni
crittografiche. Spesso per mantenere al sicuro i segreti si devono apportare modifiche
al programma principale. E, naturalmente, il programma principale gestisce anche dati
che dovrebbero rimanere riservati; altrimenti non avrebbe affatto bisogno della libreria
crittografica. Torniamo così a un punto familiare: le considerazioni sulla sicurezza coinvolgono ogni parte del sistema.

120   Capitolo 8

8.4 Qualità del codice
Se state realizzando un’implementazione di un sistema crittografico, dovrete passare molto
tempo a occuparvi della qualità. Questo non è un libro di programmazione, tuttavia
riteniamo utile spendere parola sulla qualità del codice.

8.4.1 Semplicità
La complessità è il nemico principale della sicurezza. Pertanto, ogni progetto di sicurezza
dovrebbe puntare strenuamente alla semplicità. Occorre essere intransigenti a questo
riguardo, anche a costo di perdere un po’ di popolarità. Eliminate tutte le opzioni
possibili. Liberatevi di tutte quelle caratteristiche barocche usate solo da pochi. State
lontani dai progetti cosiddetti “di comitato”, perché finiscono sempre per aggiungere
caratteristiche o opzioni extra per raggiungere i necessari compromessi. Nella sicurezza
deve regnare la semplicità.
Un classico esempio è offerto dal nostro canale sicuro. Non ha opzioni particolari. Non
consente di cifrare i dati senza autenticarli, o di autenticare i dati senza cifrarli. La gente
chiede sempre queste funzionalità, ma in molti casi non è in grado di comprendere le
conseguenze dell’utilizzo di funzionalità di sicurezza incomplete. La gran parte degli
utenti non ha conoscenze di sicurezza sufficienti per essere in grado di scegliere le opzioni
corrette. La soluzione migliore è quella di non offrire opzioni e rendere il sistema sicuro
di default. Se proprio siete costretti, offrite una sola scelta: sicuro o insicuro.
Molti sistemi hanno anche più gruppi di cifrari e offrono all’utente (o a qualcun altro)
la scelta di quale cifrario e quale funzione di autenticazione utilizzare. Quando possibile,
eliminate questa complessità. Scegliete una sola modalità che sia abbastanza sicura per
tutte le applicazioni. La differenza in termini computazionali tra le diverse modalità
crittografiche non è poi così elevata, e la crittografia rappresenta raramente un collo di
bottiglia per i computer moderni. Oltre a liberarvi della complessità, così vi mettete al
riparo dal pericolo che gli utenti possano configurare le proprie applicazioni in modo
da utilizzare un gruppo di cifrari deboli. Dopo tutto, se la scelta di una modalità di cifratura e di autenticazione è decisamente difficile perfino per il progettista, prendere una
decisione fondata sarà ancora più complicato per un semplice utente.

8.4.2 Modularità
Anche dopo avere eliminato molte opzioni e funzionalità, il sistema finale rimarrà comunque piuttosto complesso. Esiste una tecnica per rendere gestibile questa complessità:
la modularità. Il sistema viene suddiviso in moduli separati, che vengono progettati,
analizzati e implementati separatamente.
Dovreste avere già una certa familiarità con la modularità; nella crittografia diventa ancora
più importante realizzarla in maniera corretta. Abbiamo parlato precedentemente delle
primitive crittografiche indicandole come moduli. L’interfaccia del modulo dovrebbe essere semplice e chiara. Dovrebbe comportarsi secondo le aspettative ragionevoli
dell’utente. Prestate grande attenzione alle interfacce dei vostri moduli. Spesso vi sono
opzioni o caratteristiche che esistono solo per risolvere i problemi di un altro modulo;
se possibile, eliminatele. Ogni modulo dovrebbe essere in grado di risolvere i propri

Aspetti relativi all’implementazione (I)   121

problemi. Abbiamo visto che, quando le interfacce dei moduli iniziano a sviluppare
caratteristiche strane, è arrivato il momento riprogettare il software, perché questo è un
sintomo di carenze progettuali.
La modularità è così importante perché costituisce l’unico modo efficiente che abbiamo
per gestire la complessità. Se una determinata opzione è limitata a un solo modulo, può
essere analizzata nel contesto di questo modulo. Se però l’opzione modifica il comportamento esterno del modulo, allora potrà avere effetto anche sugli altri. Se avete 20
moduli, ognuno con una singola opzione binaria che ne modifica il comportamento,
saranno possibili oltre un milione di configurazioni. Dovreste analizzarle tutte una per
una per verificarne la sicurezza: un compito impossibile.
Abbiamo visto che molte opzioni vengono create nella ricerca dell’efficienza. Si tratta
di un problema ben conosciuto nell’ingegneria del software. Molti sistemi contengono
cosiddette ottimizzazioni che sono inutili, controproducenti, o insignificanti perché
non ottimizzano le parti del sistema che formano il collo di bottiglia. Siamo diventati
piuttosto prudenti riguardo le ottimizzazioni; di solito non ci perdiamo troppo tempo.
Creiamo un progetto attento e cerchiamo di assicurare che il lavoro possa essere svolto
a grosse “porzioni”. Un esempio tipico è il BIOS dei vecchi PC IBM. La routine per
scrivere un carattere sullo schermo accettava come argomento un singolo carattere. Questa
routine impiegava quasi tutto il tempo in attività di sovraccarico, e solo una frazione
di esso per l’effettiva visualizzazione del carattere. Se l’interfaccia della routine avesse
accettato una stringa come argomento, questa avrebbe potuto essere visualizzata in un
tempo solo leggermente più lungo di quello impiegato per scrivere un singolo carattere. Il risultato di questa cattiva progettazione era che tutte le macchine DOS avevano
uno schermo tremendamente lento. Questo stesso principio vale per la progettazione
crittografica. Assicuratevi che il lavoro possa essere svolto a porzioni sufficientemente
grandi. Quindi, ottimizzate solo le parti del programma di cui potete misurare l’effetto
sulle prestazioni.

8.4.3 Asserzioni
Le asserzioni sono un buon strumento per cercare di migliorare la qualità del codice.
Quando implementate del codice crittografico, assumete un atteggiamento di paranoia
professionale. Ogni modulo non si deve fidare degli altri, deve verificare sempre la validità
dei parametri, deve attivare restrizioni sulla sequenza delle chiamate e rifiutare operazioni
non sicure. Il più delle volte queste sono asserzioni semplici. Se le specifiche del modulo
indicano che un oggetto deve essere inizializzato prima di poter essere usato, allora l’uso
di un oggetto prima della sua inizializzazione produrrà un errore di asserzione. Errori
di questo tipo dovrebbero sempre portare a una chiusura del programma con un’ampia
spiegazione relativa all’istruzione in errore e alle cause.
La regola generale è questa: ogni volta che potete svolgere una verifica significativa sulla
coerenza interna del sistema, dovreste inserire un’asserzione. Cercate di intercettare il
maggior numero possibile di errori, che siano commessi da voi o da altri programmatori. Un errore intercettato da un’asserzione non porterà a una compromissione della
sicurezza.
Alcuni programmatori implementano il controllo delle asserzioni in fase di sviluppo, ma
lo disattivano quando rilasciano il prodotto. Questa non è la prospettiva della sicurezza.
Che cosa pensereste di una centrale nucleare dove gli operatori si esercitano con tutti

122   Capitolo 8

i sistemi di sicurezza attivi, ma poi quando vanno al lavoro sul vero reattore li disattivano? O di un paracadutista che indossa il paracadute di emergenza negli allenamenti a
terra ma lo lascia sull’aereo al momento del salto? Perché mai si dovrebbe disabilitare il
controllo delle asserzioni nel codice di produzione? È l’unico posto dove serve davvero!
Se un’asserzione fallisce in produzione, avete appena incontrato un errore di programmazione; ignorarlo porterà molto probabilmente a qualche tipo di risposta sbagliata,
perché almeno una delle ipotesi che il codice fa è sbagliata. Generare risposte errate è
probabilmente la cosa peggiore che un programma può fare. È molto meglio, quantomeno, informare l’utente che si è verificato un errore di programmazione, in modo che
non si fidi dei risultati errati del programma. La nostra raccomandazione è di lasciare
attivo il controllo degli errori.

8.4.4 Buffer overflow
I problemi di buffer overflow sono noti da decenni, e da decenni sono disponibili soluzioni
perfette per evitarli. Alcuni dei primissimi linguaggi di programmazione ad alto livello,
come Algol 60, risolvevano il problema in maniera definitiva introducendo il controllo
obbligatorio sui limiti degli array. Nonostante tutto, i buffer overflow provocano un
gran numero di problemi di sicurezza su Internet. Esistono anche diversi altri attacchi
software, come gli attacchi alla vulnerabilità di formato della stringa o gli attacchi di
tipo integer overflow.
Ma queste sono cose che non possiamo cambiare. Possiamo darvi consigli su come scrivere codice crittografico di qualità. Evitate qualsiasi linguaggio di programmazione che
consenta i buffer overflow. Nello specifico: non usate C o C++. E non disattivate mai il
controllo sui limiti degli array, qualsivoglia linguaggio decidiate di utilizzare. È una regola
semplice, e probabilmente risolverà una buona metà dei vostri problemi di sicurezza.

8.4.5 I test
Una fase accurata di test fa sempre parte di ogni processo di sviluppo valido. I test possono essere utili per individuare i bug dei programmi, ma non per individuare i bug
nella sicurezza. Non confondete mai i test con l’analisi della sicurezza: sono attività
complementari, ma diverse.
I tipi di test che dovrebbero essere implementati sono due. Il primo è un gruppo di test
derivato dalle specifiche funzionali del modulo. Idealmente, un programmatore dovrebbe
implementare un modulo e un secondo programmatore implementare i test. Entrambi
lavorano sulla base delle specifiche funzionali. Qualsiasi incomprensione tra i due è un
chiaro segno del fatto che le specifiche debbano essere chiarite. I test generici dovrebbero cercare di coprire l’intero spettro operativo del modulo. Per alcuni moduli questo
è semplice; per altri, il programma di test dovrà simulare un ambiente completo. Anche
nel codice che noi sviluppiamo, la parte dedicata al test ha quasi le stesse dimensioni di
quella operativa, e non siamo riusciti a trovare un modo per migliorare la situazione in
maniera significativa.
Un secondo gruppo di test viene implementato dallo stesso programmatore del modulo. Sono verifiche progettate per individuare eventuali limiti di implementazione. Per
esempio, se un modulo internamente usa un buffer di 4 KB, allora dei test aggiuntivi

Aspetti relativi all’implementazione (I)   123

sui limiti superiori e inferiori del buffer aiuteranno a individuare eventuali errori di
gestione del buffer stesso. A volte per approntare test particolari è necessario conoscere
i meccanismi interni di un modulo.
Scriviamo spesso sequenze di test ricavate da un generatore casuale. Discuteremo in
maniera approfondita i generatori di numeri pseudocasuali (PRNG) nel Capitolo 9.
L’uso di un PRNG facilita notevolmente l’esecuzione di un gran numero di test. Se
salviamo il seme che usiamo per il PRNG, siamo in grado di ripetere la stessa sequenza
di test, il che è molto utile per le attività di verifica e di debugging. I dettagli dipendono
dal modulo in questione.
Infine, abbiamo trovato utile disporre di codice di “test rapido” che viene eseguito a ogni
avvio di un programma. In uno dei suoi progetti, Niels Ferguson dovette implementare
AES. Il codice di inizializzazione eseguiva AES su alcuni casi di test e verificava l’output
ottenuto rispetto alle risposte corrette. Se mai il codice AES dovesse venire destabilizzato nel corso dello sviluppo successivo, questo veloce test riuscirà verosimilmente a
individuare il problema.

8.5 Attacchi a canale laterale
Esiste tutta una classe di attacchi chiamati a canale laterale [72]. Questi sono possibili quando
un attaccante dispone di un canale aggiuntivo di informazioni sul sistema. Per esempio,
un attaccante potrebbe cronometrare con precisione il tempo necessario per cifrare un
messaggio. In base alle modalità di implementazione del sistema, queste informazioni
temporali potrebbero consentire all’attaccante di dedurre informazioni private sul messaggio stesso o sulla chiave di cifratura sottostante. Se la crittografia è incorporata in una
smart card, l’attaccante può misurare l’assorbimento di corrente della carta nel tempo. I
campi magnetici, le emissioni di radiofrequenze, il consumo di corrente, la misurazione
dei tempi e l’interferenza su altri canali di dati sono tutti elementi che possono essere
utilizzati per gli attacchi a canale laterale.
Non sorprende che gli attacchi a canale laterale possano essere particolarmente efficaci
contro sistemi che non sono stati progettati tenendone conto. L’analisi della tensione
delle smart card è estremamente efficace [77].
È molto difficile, se non impossibile, proteggersi da tutti i tipi di attacchi a canale laterale,
ma è possibile prendere qualche semplice precauzione. Anni fa, mentre Niels Ferguson
stava lavorando all’implementazione di sistemi crittografici nelle smart card, una delle
regole di progettazione era che la sequenza di istruzioni eseguita dalla CPU potesse
dipendere solamente da informazioni già disponibili per l’attaccante. Questo blocca gli
attacchi a tempo, e rende più complicati gli attacchi basati sull’analisi della tensione, perché
la sequenza delle istruzioni eseguite non può più rivelare alcuna informazione. Non è
una soluzione completa, e le moderne tecniche di analisi della tensione non avrebbero
difficoltà a compromettere le smart card utilizzate a quel tempo.Tuttavia, quel rimedio era
quasi il meglio che si potesse fare in quelle situazioni. La resistenza agli attacchi a canale
laterale potrà sempre derivare da una combinazione di contromisure, alcune presenti nel
software che implementa il sistema crittografico, altre presenti nell’hardware reale.
La prevenzione degli attacchi a canale laterale è una sorta di corsa agli armamenti. Si
cerca di proteggersi dagli attacchi conosciuti, e poi qualche tipo brillante scopre un
altro canale e occorre ricominciare tenendo conto anche di quello. Nella vita reale la

124   Capitolo 8

situazione non è così grave, perché la maggior parte degli attacchi a canale laterale è
difficile da condurre. I canali laterali costituiscono un pericolo reale per le smart card,
perché le carte sono sotto il pieno controllo dell’avversario, ma solo pochi degli altri
attacchi sono efficaci contro la gran parte dei computer. Nella pratica, i canali laterali
più importanti sono la misurazione dei tempi e le emissioni di radiofrequenze (le smart
card sono particolarmente vulnerabili alle misurazioni del consumo elettrico).

8.6 Conclusioni e altre letture
Speriamo che questo capitolo abbia chiarito che la sicurezza non inizia e non finisce
con la progettazione crittografica.Tutti gli aspetti del sistema devono fare la propria parte
per raggiungere la sicurezza.
L’implementazione dei sistemi crittografici è un’arte a sé. L’aspetto più importante è la
qualità del codice. Un codice di bassa qualità è la causa più comune degli attacchi nel
mondo reale, e può essere evitato piuttosto facilmente. Per la nostra esperienza, scrivere
codice di alta qualità non richiede tempi troppo diversi dallo scrivere codice di bassa
qualità, se si misura il tempo dall’avvio del progetto al prodotto finito, e non dall’inizio
al rilascio delle prima versione difettosa. Diventate fanatici della qualità del vostro codice.
Potete farlo, e dovete farlo, quindi fatelo!
Citiamo alcuni libri utili per approfondire questi temi: Software Security: Building Security
In di McGraw [88], The Security Development Lifecycle di Howard e Lipner [62], The Art
of Software Security Assessment: Identifying and Preventing Software Vulnerabilities di Dowd,
McDonald e Schuh [37].

8.7 Esercizi
Esercizio 8.1 Descrivete come ciascuno degli elementi descritti nel Paragrafo 8.3 si
applichi alla configurazione hardware e software del vostro computer.
Esercizio 8.2 Trovate un nuovo prodotto o sistema in grado di manipolare i dati in
transito. Potrebbe trattarsi di quello che avete analizzato nell’Esercizio 1.8. Effettuate una
revisione della sicurezza del prodotto o sistema come descritto nel Paragrafo 1.12, ma
questa volta focalizzando l’attenzione sui problemi che riguardano le modalità con cui
il sistema potrebbe memorizzare i segreti (Paragrafo 8.3).
Esercizio 8.3 Trovate un nuovo prodotto o sistema in grado di manipolare i dati
segreti. Potrebbe trattarsi di quello che avete analizzato nell’Esercizio 1.8. Effettuate una
revisione della sicurezza del prodotto o sistema come descritto nel Paragrafo 1.12, ma
questa volta focalizzandovi sugli aspetti relativi alla qualità del codice (Paragrafo 8.4).
Esercizio 8.4 Controllate la mailing list di bugtraq per una settimana. Create una
tabella che contenga tutti i diversi tipi di vulnerabilità annunciati o risolti nella settimana,
e anche il numero delle vulnerabilità per ciascun tipo. Che tipo di deduzioni si possono
trarre da questa tabella? Fate riferimento a http://www.schneier.com/ce.html per ulteriori
informazioni sulla mailing list di bugtraq.

Parte III

Negoziazione
della chiave
In questa parte
• Capitolo 9 Generazione di dati casuali
• Capitolo 10 I numeri primi
• Capitolo 11 Diffie-Hellman
• Capitolo 12 RSA
• Capitolo 13 Introduzione
ai protocolli crittografici
• Capitolo 14 Il protocollo di negoziazione
della chiave
• Capitolo 15 Aspetti relativi
all’implementazione (II)

Capitolo 9

Generazione di dati
casuali
Per generare la chiave serve un generatore di numeri
casuali, RNG (Random Number Generator). La generazione di dati casuali è fondamentale per molte attività
crittografiche, ed eseguirla bene è molto difficile.
Non entreremo nei dettagli del significato reale di
casualità, per i nostri scopi è sufficiente una discussione informale. Una buona definizione informale
è la seguente: i dati casuali sono imprevedibili per
un attaccante, anche se questi tenta attivamente di
individuare uno schema che rompa la casualità che
abbiamo generato.
Generatori di numeri casuali ben fatti sono necessari
per molte funzioni crittografiche. Nella Parte II abbiamo trattato il canale sicuro e i suoi componenti,
supponendo che vi fosse una chiave conosciuta sia
da Alice sia da Bob. Tale chiave deve essere generata,
e a questo scopo si utilizzano generatori di numeri
casuali. Utilizzando un generatore di numeri casuali
non adatto, si otterrà una chiave debole, come è
avvenuto per una delle prime versioni del browser
Netscape [54].
La casualità si misura mediante l’entropia [118]. Il
concetto di base è il seguente: se avete una parola di
32 bit completamente casuale, ha 32 bit di entropia.
Se la parola di 32 bit assume soltanto quattro valori
diversi, con probabilità del 25% di presentarsi ciascuno, ha 2 bit di entropia. L’entropia non misura il
numero di bit di un valore, ma l’incertezza relativa
al valore. Potete considerarla come il numero di bit
medio che servirebbe per specificare il valore potendo
utilizzare un algoritmo di compressione ideale. Notate che l’entropia di un valore dipende da quanto si
sa su di esso. Una parola casuale di 32 bit ha 32 bit di

Sommario
9.1 Casualità nei dati del
mondo reale
9.2 Modelli di attacco per un
PRNG
9.3 Fortuna
9.4 Il generatore
9.5 L’accumulatore
9.6 Gestione del file di seme
9.7 Scelta di elementi casuali
9.8 Esercizi

128   Capitolo 9

entropia. Ora supponete di venire a conoscenza del fatto che il valore ha esattamente 18
bit impostati a 0 e 14 bit impostati a 1. Ci sono circa 228,8 valori che soddisfano questi
requisiti, quindi l’entropia è di soli 28,8 bit. In altre parole, più informazioni avete un
valore, minore è la sua entropia.
È un po’ più complicato calcolare l’entropia per valori con distribuzione di probabilità
non uniforme. La definizione più comune di entropia per una variabile X è:

H ( x ) := −∑ P ( X = x ) log 2 P ( X = x )
X

dove P(X = x) è la probabilità che la variabile X assuma il valore x. Non utilizzeremo
questa formula, perciò non occorre che la ricordiate. Questa è la definizione a cui si
riferiscono la maggior parte dei matematici quando parlano di entropia. Esistono altre
definizioni di entropia utilizzate dai matematici, in realtà la scelta dipende da ciò su cui
si lavora. La nostra definizione di entropia, comunque, non va confusa con quella dei
fisici, che si riferiscono a un concetto della termodinamica del tutto diverso, anche se
con qualche affinità.

9.1 Casualità nei dati del mondo reale
In un mondo ideale utilizzeremmo dati “casuali reali”, tratti da fenomeni reali, ma il
nostro mondo non è ideale ed è estremamente difficile trovare dati di questo tipo.
I computer tipici hanno varie sorgenti di entropia. Esempi ben noti sono gli istanti
esatti in cui vengono premuti i tasti e i movimenti esatti di un mouse. Si sono studiate
anche le fluttuazioni casuali dei tempi di accesso dei dischi fissi, causate da turbolenze
all’interno del contenitore [29]. Tutte queste sorgenti sono in qualche modo sospette,
perché esistono situazioni in un attaccante può influenzarle o misurarle.
Si sarebbe tentati dal considerare con ottimismo la quantità di entropia che è possibile
ricavare da varie sorgenti. Abbiamo visto software in grado di generare 1 o 2 byte di dati
ritenuti casuali dal timing di una singola pressione di un tasto. Gli esperti di crittografia,
però, in generale sono molto più pessimistici su questo aspetto. Un buon dattilografo
è in grado di mantenere costante il tempo che trascorre tra le pressioni dei tasti, con
una tolleranza di una dozzina di millisecondi. Inoltre, la frequenza di scansione della
tastiera limita la risoluzione con cui si possono misurare gli istanti in cui i tasti vengono
premuti. Anche i dati digitati non sono molto casuali, anche se si chiede all’utente di
digitare semplicemente qualcosa per generare dati casuali. Inoltre, c’è sempre il rischio
che l’attaccante disponga di informazioni aggiuntive sugli eventi “casuali”. Con un
microfono potrebbe registrare i suoni della tastiera, in modo da determinare gli istanti
in cui sono premuti i tasti. Prestate molta attenzione a stimare la quantità di entropia
contenuta in un insieme di dati; tenete sempre conto che l’avversario è molto astuto e
agisce in modo attivo.
Molti processi fisici hanno un comportamento casuale. Per esempio, le leggi della fisica
quantistica impongono la perfetta casualità di determinati comportamenti. Sarebbe bello
poter misurare tali comportamenti, e tecnicamente è possibile farlo, tuttavia, l’attaccante
dispone di alcune strare per attaccare questo tipo di soluzione. Per prima cosa, l’attaccante
può tentare di influenzare il comportamento delle particelle quantistiche per fare in modo
che si comportino in modo prevedibile. Può anche tentare di intercettare le misurazio-

Generazione di dati casuali   129

ni effettuate; se si procura una copia dei dati rilevati, anche se questi sono casuali, dal
punto di vista dell’attaccante non conterranno alcuna entropia (se ne conosce il valore,
l’entropia è nulla). L’attaccante potrebbe generare un forte campo a radiofrequenza nel
tentativo di influenzare il nostro rilevatore. Esistono anche alcuni attacchi basati sulla
fisica quantistica. Il paradosso di Einstein-Podolsky-Rosen potrebbe essere utilizzato per
abbattere la casualità che stiamo tentando di misurare [11, 19]. Considerazioni analoghe
si potrebbero fare per altre sorgenti di entropia, quali il rumore termico di un resistore,
l’effetto tunnel e il rumore di rottura di un diodo Zener.
Alcuni computer moderni dispongono di un generatore di numeri casuali integrato [63].
Questo rappresenta un miglioramento significativo rispetto a un generatore separato,
poiché aumenta la difficoltà di alcuni tipi di attacchi. Il generatore di numeri casuali
interno è accessibile soltanto al sistema operativo, perciò un’applicazione deve affidarsi
a quest’ultimo per gestire i dati casuali in modo sicuro.

9.1.1 Problemi nell’utilizzo di dati casuali reali
L’utilizzo di dati casuali reali, a parte la difficoltà di raccoglierli, pone diversi altri problemi.
In primo luogo, questi dati non sono sempre disponibili. Se dovete attendere che vengano
premuti dei tasti, non potete ottenere alcun dato casuale se l’utente non digita qualcosa;
questo può essere un serio problema nel caso di un server web su una macchina priva
di tastiera. Un altro problema è che la quantità di dati casuali reali è sempre limitata; se
ve ne servono molti dovrete attendere, cosa inaccettabile per molte applicazioni.
Un altro problema è che le sorgenti di casualità reali, come i generatori di numeri casuali
di tipo fisico, possono guastarsi. Un generatore fisico potrebbe diventare prevedibile. Si
tratta di dispositivi complessi inseriti nell’ambiente molto rumoroso di un computer, che
si guastano più facilmente rispetto ai componenti tradizionali. Se vi affidate direttamente
a un generatore di dati reali e questo si rompe, è un grosso guaio. Inoltre, cosa ancora
peggiore, non sempre ci si accorge dei guasti.
Un ulteriore problema è quello di giudicare la quantità di entropia che è possibile
estrarre da uno specifico evento fisico. Se non si dispone di un hardware dedicato per il
generatore di dati casuali, è estremamente difficile sapere quanta entropia si ottiene. Ne
parleremo meglio più avanti.

9.1.2 Dati pseudocasuali
Un’alternativa all’utilizzo di dati casuali reali è quella di ricorrere a dati pseudocasuali.
Questi non sono realmente casuali, ma sono generati da un algoritmo deterministico
partendo da un seme (seed). Se si conosce il seme, è possibile prevedere il dato pseudocasuale. I generatori di numeri pseudocasuali tradizionali, o PRNG (PseudoRandom Number
Generator) non sono sicuri contro un avversario astuto; sono progettati per eliminare
artefatti statistici, non per combattere contro un attaccante intelligente. Il secondo volume
di The Art of Computer Programming di Knuth contiene un’ampia discussione dei generatori
di numeri casuali, ma tutti i generatori sono analizzati soltanto dal punto di vista della
casualità statistica [75]. Dobbiamo ipotizzare che il nostro avversario conosca l’algoritmo
utilizzato per generare i dati casuali. Data una parte dell’output pseudocasuale, è possibile
prevedere alcuni bit casuali futuri (o passati)? Per molti PRNG tradizionali la risposta
potrebbe essere sì, mentre per un PRNG crittografico ben fatto la risposta è no.

130   Capitolo 9

Nel contesto di un sistema crittografico ci sono requisiti più stringenti. Anche se l’attaccante vede buona parte dei dati casuali generati dal PRNG, non dovrebbe essere in
grado di prevedere alcunché sul resto dell’output. Un PRNG di questo tipo è detto
crittograficamente forte. Poiché i PRNG tradizionali non ci servono, parleremo soltanto
dei PRNG crittograficamente forti.
Dimenticate la normale funzione random presente nella vostra libreria di programmazione, perché quasi certamente non offre un PRNG crittografico. In effetti non si deve
mai utilizzare un PRNG di libreria a meno che non sia fornita documentazione del
fatto che è crittograficamente forte.

9.1.3 Dati casuali reali e PRNG
Utilizziamo dati casuali reali per un unico scopo: fare da seme per un PRNG. In questo
modo risolviamo parte dei problemi legati all’utilizzo di dati casuali reali. Una volta
fornito il seme al PRNG, sono sempre disponibili dei dati casuali. Potete continuare ad
aggiungere al seme del PRNG i dati casuali reali che ottenete, assicurandovi così che il
generatore non sarà mai del tutto prevedibile anche qualora il seme sia scoperto.
Secondo un argomento teorico, i dati casuali reali sono preferibili ai dati pseudocasuali
ottenuti da un PRNG. In alcuni protocolli crittografici è possibile dimostrare che certi
attacchi sono impossibili se si utilizzano dati casuali reali. Il protocollo è incondizionatamente sicuro. Se utilizzate un PRNG, il protocollo è sicuro soltanto se l’attaccante non
è in grado di violare il PRNG, quindi è computazionalmente sicuro. Questa distinzione,
tuttavia, ha importanza soltanto dal punto di vista teorico. Tutti i protocolli crittografici
utilizza ipotesi computazionali per quasi tutto. Rimuovere l’ipotesi computazionale per
un particolare tipo di attacco rappresenta un miglioramento insignificante, mentre generare dati casuali reali, necessari per la sicurezza incondizionata, è talmente difficile che
il ricorso a tali dati aumenta la probabilità di ridurre la sicurezza del sistema. Qualsiasi
punto debole del generatore di dati casuali reali porta immediatamente a una perdita
di sicurezza. Invece, se si utilizzano dati casuali reali per il seme di un PRNG, si può
procedere con maggiore prudenza nelle ipotesi relative alle sorgenti di entropia, perciò
aumentano le probabilità di ottenere un sistema sicuro.

9.2 Modelli di attacco per un PRNG
Il compito di generare numeri pseudocasuali a partire da un seme è abbastanza semplice.
Il problema è come ottenere un seme casuale e come tenerlo segreto in uno scenario
del mondo reale [71]. Uno dei migliori progetti, per l’attuale livello delle nostre conoscenze, è Yarrow [69], che abbiamo creato qualche anno fa insieme a John Kelsey. Questo
progetto cerca di evitare tutti gli attacchi noti.
In qualsiasi istante di tempo il PRNG ha uno stato interno. Le richieste di dati casuali
sono soddisfatte utilizzando un algoritmo crittografico per generare dati pseudocasuali.
Questo algoritmo aggiorna lo stato interno per garantire che alla richiesta successiva non
vengano forniti gli stessi dati. Il processo è semplice, si può utilizzare qualsiasi funzione
di hash o cifrario a blocchi.
Esistono varie forme di attacchi a un PRNG. C’è un attacco semplice in cui l’attaccante
tenta di ricostruire lo stato interno a partire dall’output; si tratta di un attacco classico,
abbastanza semplice da contrastare utilizzando tecniche di crittografia.

Generazione di dati casuali   131

Le cose si fanno più difficili se l’attaccante a un certo punto è in grado di acquisire lo
stato interno. Per gli scopi della nostra discussione, non è importante il modo in cui ci
riesce. Potrebbe esserci un difetto nell’implementazione, o forse il computer è stato appena
avviato per la prima volta e non ha ancora un seme per la generazione di dati casuali,
o forse l’attaccante è riuscito a leggere il file del seme dal disco. Le difficoltà esistono e
vanno affrontate. In un PRNG tradizionale, se l’attaccante acquisisce lo stato interno, può
seguire tutti gli output e tutti gli aggiornamenti dello stato interno; questo significa che,
se il PRNG viene attaccato con successo, non può mai tornare su uno stato sicuro.
Un altro problema nasce se lo stesso stato del PRNG viene utilizzato più di una volta,
cosa che può accadere quando due o più macchine virtuali (VM) sono avviate dallo
stesso stato e leggono lo stesso file di seme.
Recuperare un PRNG con stato interno compromesso è difficile, come lo è evitare il riuso
dello stesso stato da parte di più VM avviate dalla stessa istanza. Ci servirà una sorgente
di entropia data da un generatore di numeri casuali reali. Per semplicità assumeremo di
avere una o più sorgenti in grado di fornire una certa quantità di entropia (generalmente
in piccole porzioni che chiamiamo eventi) in istanti imprevedibili.
Anche se inseriamo le piccole quantità di entropia ottenute da un evento nello stato
interno, rimane una strada di attacco. L’attaccante effettua frequenti richieste di dati casuali al PRNG: finché la quantità totale di entropia aggiunta tra due di queste richieste è
limitata a 30 bit, per esempio, l’attaccante può semplicemente provare tutte le possibilità
per gli input casuali e ricavare il nuovo stato interno ottenuto dopo l’inserimento delle
dosi di entropia. Per fare ciò sarebbero necessari circa 230 passaggi, un numero non eccessivo (in realtà i nostri calcoli non sono rigorosi; in questo caso dovremmo utilizzare
la guessing entropy anziché l’entropia di Shannon standard. Per dettagli approfonditi sulle
misure di entropia, cfr. [23]). I dati casuali generati dal PRNG forniscono la necessaria
verifica quando l’attaccante arriva alla soluzione corretta.
La miglior difesa contro questo particolare attacco consiste nel riunire in un pool gli
eventi in arrivo contenenti entropia. Si raccoglie l’entropia finché si raggiunge una
quantità sufficiente per inserirla nello stato interno senza che l’attaccante sia in grado
di determinare per tentativi i dati del pool. Che quantità serve? Vogliamo che l’attaccante debba effettuare almeno 2128 passaggi per ogni attacco, perciò servono 128 bit di
entropia. Ma c’è un problema: stimare la quantità di entropia è estremamente difficile,
se non impossibile. Molto dipende da quanto l’attaccante sa o può venire a sapere, ma
tale informazione non è disponibile agli sviluppatori durante la fase di progettazione
dell’applicazione. Questo è il problema principale diYarrow; esso infatti tenta di misurare
l’entropia di una sorgente utilizzando uno stimatore, ma è impossibile realizzare uno
stimatore di entropia corretto per tutte le situazioni.

9.3 Fortuna
Nella pratica, probabilmente sceglierete di utilizzare un PRNG crittografico fornito da
una libreria ben nota. A solo scopo illustrativo, nel seguito esaminiamo la progettazione
di un PRNG che chiameremo Fortuna. Si tratta di un miglioramento di Yarrow e deve
il suo nome alla dea della fortuna per gli antichi Romani (avevamo pensato di chiamarlo
Tiche dal nome della dea della fortuna per gli antichi Greci, ma è un nome difficile da
pronunciare). La parte restante di questo capitolo tratta i dettagli di Fortuna.

132   Capitolo 9

Fortuna contiene tre parti: il generatore accetta un seme di dimensione fissa e genera
quantità arbitrarie di dati pseudocasuali; l’accumulatore raccoglie e mette insieme entropia
da varie sorgenti e occasionalmente esegue il reset del generatore; infine, il controllo del
file di seme assicura che il PRNG possa generare dati casuali anche quando il computer
è stato appena avviato.

9.4 Il generatore
Il generatore è la parte che converte uno stato a dimensione fissa in output di lunghezza
arbitraria. Utilizzeremo a questo scopo un cifrario a blocchi simile ad AES, ma potete scegliere liberamente tra AES (Rijndael), Serpent o Twofish. Lo stato interno del generatore
è costituito da una chiave di cifrario a blocchi da 256 bit e da un contatore a 128 bit.
In sostanza il generatore è semplicemente un cifrario a blocchi in modalità contatore. La
modalità CTR genera un flusso di dati casuale, che sarà il nostro output. Sono previsti
alcuni raffinamenti.
Se un utente o un’applicazione chiede dati casuali, il generatore esegue il proprio algoritmo e genera dati pseudocasuali. Ora supponete che un attaccante riesca a compromettere
lo stato del generatore dopo il completamento della richiesta; sarebbe bello se questo
non compromettesse i precedenti risultati forniti dal generatore. Quindi, dopo ogni richiesta generiamo altri 256 bit di dati pseudocasuali e li utilizziamo come nuova chiave
per il cifrario a blocchi. Possiamo quindi dimenticare la vecchia chiave, eliminando così
qualsiasi possibilità di perdere informazioni su richieste precedenti.
Per assicurarci che i dati generati saranno statisticamente casuali, non possiamo generarne
troppi alla volta. Dopo tutto, in dati puramente casuali possono esserci valori a blocchi
ripetuti, ma l’output della modalità contatore non contiene mai valori ripetuti (cfr. il
Paragrafo 4.8.2 per i dettagli). Esistono varie soluzioni; potremmo utilizzare solo una
metà di ciascun blocco di testo cifrato, in modo da occultare gran parte della deviazione
statistica. Potremmo utilizzare un diverso blocco denominato funzione pseudocasuale,
anziché un cifrario a blocchi, ma non vi sono proposte ben analizzate ed efficienti, per
quanto ne sappiamo. La soluzione più semplice è quella di limitare il numero di byte
di dati casuali in una singola richiesta, rendendo molto più difficile da individuare la
deviazione statistica.
Se volessimo generare 264 blocchi di output da una singola chiave, ci aspetteremmo
circa una collisione sui valori dei blocchi. Poche richieste ripetute di questa dimensione
mostrerebbero rapidamente che l’output non è perfettamente casuale: manca infatti
delle collisioni previste. Limitiamo la dimensione massima di una singola richiesta a 216
blocchi (220 byte). Per un generatore di dati casuali ideale, la probabilità di una collisione
dei valori dei blocchi in 216 blocchi di output è circa 2−97, perciò la totale assenza di
collisioni non sarebbe rilevabile fino a circa 297 richieste effettuate. Il carico di lavoro
totale per l’attaccante arriva così a 2113 passaggi. Non siamo ancora ai 2128 passaggi che
rappresentano il nostro obiettivo, ma ci siamo abbastanza vicini.
Sappiamo che in questo caso stiamo accettando un livello di sicurezza (leggermente)
ridotto. D’altra parte, non sembra esserci una buona alternativa. Non disponiamo di
componenti crittografici elementari in grado di fornire un PRNG con sicurezza a 128
bit. Potremmo utilizzare SHA-256, ma sarebbe molto più lento. Abbiamo rilevato che
molte persone fanno di tutto per non utilizzare un buon PRNG crittografico, e la velocità

Generazione di dati casuali   133

è sempre stata una buona scusa. Rallentare il PRNG di un fattore percettibile allo scopo
di ottenere pochi bit in più di sicurezza è controproducente: troppe persone preferiranno
passare a un pessimo PRNG e così la sicurezza complessiva del sistema crollerà.
Se avessimo un cifrario a blocchi con dimensione dei blocchi di 256 bit, le collisioni
non sarebbero un problema. Questo particolare tipo di attacco non rappresenta una
grande minaccia; l’attaccante deve eseguire 2113 passaggi, ma il computer attaccato deve
eseguire 2113 cifrature, perciò tutto dipende dalla velocità del computer dell’utente e non
di quello dell’attaccante. La maggior parte degli utenti, però, non aggiunge potenza di
elaborazione solo per venire incontro a un attaccante. Non amiamo questi tipi di strategie
sulla sicurezza; sono complicate, e se il PRNG fosse utilizzato in una situazione inusuale,
potrebbero non risultare più valide. Comunque, data la situazione, la soluzione fornita
è il miglior compromesso che siamo stati in grado di trovare.
Quando reimpostiamo la chiave del cifrario a blocchi al termine di ogni richiesta, non
eseguiamo il reset del contatore. È un aspetto secondario, ma evita problemi con i cicli
brevi. Supponiamo di dover resettare il contatore ogni volta. Se un valore della chiave
si ripete, e tutte le richieste hanno dimensione fissa, allora anche il successivo valore
della chiave sarebbe ripetuto. Potremmo così cadere in un ciclo breve di valori chiave.
È una situazione improbabile, ma possiamo evitarla del tutto non eseguendo il reset del
contatore. Poiché il contatore ha dimensione di 128 bit, non ripeteremo mai uno dei
suoi valori (2128 blocchi vanno oltre le capacità di calcolo dei nostri computer) e questo
interrompe automaticamente qualsiasi ciclo. Inoltre, utilizziamo il valore del contatore
0 per indicare che il generatore non ha ancora ricevuto una chiave e perciò non può
generare alcun output.
Notate che la restrizione che limita ogni richiesta a un massimo di 1 MB di dati non è
inflessibile. Se vi servono più di 1 MB di dati casuali, basta eseguire richieste ripetute. In
effetti l’implementazione potrebbe fornire un’interfaccia che esegua automaticamente
tali richieste ripetute.
Il generatore di per sé è un modulo estremamente utile. Nell’implementazione potrebbe
essere reso disponibile come elemento dell’interfaccia, e non solo come componente,
di Fortuna. Considerate un programma che esegue una simulazione Monte Carlo (una
simulazione guidata da scelte casuali). Volete che la simulazione sia casuale, ma volete
anche essere in grado di ripetere esattamente lo stesso calcolo, anche solo per scopi di
debugging e verifica. Una buona soluzione è quella di richiamare il generatore di dati
casuali del sistema operativo, all’avvio del programma, per ottenere un seme casuale. Questo seme può essere registrato come parte dell’output del simulatore e da esso il nostro
generatore può generare tutti i dati casuali necessari per la simulazione. Conoscendo
il seme originale del generatore, inoltre, consente di verificare tutti i calcoli eseguendo
il programma di nuovo con gli stessi dati di input e lo stesso seme. Per il debugging è
possibile eseguire ripetutamente la stessa simulazione, che si comporterà allo stesso modo
ogni volta, purché il seme di partenza sia sempre lo stesso.
Ora possiamo esaminare in dettaglio le operazioni svolte dal generatore.

9.4.1 Inizializzazione
Questa fase è piuttosto semplice: impostiamo la chiave e il contatore a zero per indicare
che il generatore non ha ancora ricevuto il seme.

134   Capitolo 9

function InitializeGenerator
output: G Stato del generatore.

Imposta la chiave K e il contatore C a zero.

(K, C) ← (0, 0)

Impacchetta lo stato.

G ← (K, C)

return G

9.4.2 Nuovo seme
Questa operazione aggiorna lo stato con una stringa di input arbitraria. A questo livello
non ci interessa il contenuto di questa stringa di input. Per assicurarci di avere un buon
rimescolamento dell’input con la chiave esistente, utilizziamo una funzione di hash.
function Reseed
input: G Stato del generatore; modificato da questa funzione.

s Seme nuovo o aggiuntivo.

Calcola la nuova chiave utilizzando una funzione di hash.

K ← SHAd-256(K || s)

Incrementa il contatore in modo che non sia nullo e contrassegna il generatore in modo che si
sappia che gli è stato fornito il seme. In tutto questo generatore, C è un valore
a 16 byte considerato come intero utilizzando la convenzione per cui il primo
byte è quello meno significativo.

C←C+1
Il contatore C è utilizzato come intero. Più avanti sarà usato come blocco di testo in
chiaro. Per la conversione utilizziamo la convenzione per cui il primo byte è quello meno
significativo. Il blocco di testo intero è un blocco di 16 byte p0, ... , p15 che corrisponde
al valore intero;
15
∑ pi 28i
i =0

Utilizzando ovunque questa convenzione, possiamo considerare C sia come stringa a
16 byte, sia come intero.

9.4.3 Generazione di blocchi
Questa funzione genera un certo numero di blocchi di output casuale. Si tratta di una
funzione interna, utilizzata soltanto dal generatore. Qualsiasi entità al di fuori del PRNG
non dovrebbe essere in grado di richiamarla.
function GenerateBlocks
input: G Stato del generatore; modificato da questa funzione.

k Numero di blocchi da generare.

Generazione di dati casuali   135

output: r Stringa pseudocasuale di 16k byte.

assert C ≠ 0

Inizia con la stringa vuota.

r←ò

Aggiunge i blocchi necessari.

for i =1, ..., k do

r ← r || E(K, C)

C←C+1

od

return r
Naturalmente E(K, C) è la funzione di cifratura del cifrario a blocchi con chiave K e
testo in chiaro C. La funzione GenerateBlocks per prima cosa controlla che C sia diverso da zero, poiché ciò indica che questo generatore non ha mai ricevuto un seme. Il
simbolo ò indica la stringa vuota. Il ciclo inizia con una stringa vuota in r e poi aggiunge
a r ciascun nuovo blocco calcolato per costruire il valore di output.

9.4.4 Generazione di dati casuali
Questa funzione genera dati casuali a richiesta dell’utente. Consente di effettuare l’output
di un massimo di 220 byte e si assicura che il generatore dimentichi qualsiasi informazione
sul risultato generato.
function PseudoRandomData
input: G Stato del generatore; modificato da questa funzione.

n Numero di byte di dati casuali da generare.
output: r Stringa pseudocasuale di n byte.

Limita la lunghezza dell’output per ridurre la deviazione statistica da output perfettamente
casuali. Inoltre si assicura che la lunghezza non sia negativa.

assert 0 ≤ n ≤ 220

Calcola l’output.

r ← first-n-bytes(GenerateBlocks(G, n/16))

Passa a una nuova chiave per evitare successive compromissioni di questo output.

K ← GenerateBlocks(G, 2)

return r
L’output è generato da una chiamata di GenerateBlocks e l’unico cambiamento è che il
risultato è troncato al numero di byte corretto (l’operatore ⋅ esegue l’arrotondamento
all’intero superiore). Poi generiamo altri due blocchi per ottenere una nuova chiave. Una
volta che la vecchia chiave K è stata dimenticata, non vi è modo di ricalcolare il risultato
r. Purché PseudoRandomData non mantenga una copia di r, o dimentichi di cancellare
la memoria in cui r era memorizzata, il generatore non può lasciar fuoriuscire alcun
dato su r, una volta completata la funzione. È proprio per questo che qualsiasi futura
compromissione del generatore non potrà mettere in pericolo la segretezza degli output

136   Capitolo 9

precedenti. Metterà in pericolo la sicurezza degli output futuri, ma questo problema è
affrontato dall’accumulatore.
La funzione PseudoRandomData ha un limite alla quantità di dati che può restituire.
Si può specificare un wrapper che può restituire stringhe casuali di dimensione maggiore mediante chiamate ripetute di PseudoRandomData. Notate che non dovreste
aumentare la dimensione massima dell’output per chiamata, perché così aumenta la
deviazione statistica dalla casualità pura. Eseguire chiamate ripetute di PseudoRandomData è abbastanza efficiente; l’unico reale sovraccarico è dato dal fatto che, per ogni 1
MB di dati casuali prodotti, occorre generare 32 byte casuali aggiuntivi (per la nuova
chiave) ed eseguire nuovamente il gestore della chiave del cifrario a blocchi. Si tratta di
un sovraccarico insignificante per tutti i cifrari a blocchi da noi suggeriti.

9.4.5 Velocità del generatore
Il generatore per Fortuna appena descritto è un PRNG crittograficamente forte nel
senso che converte un seme in un output pseudocasuale di lunghezza arbitraria. È veloce
più o meno quanto il cifrario a blocchi sottostante, su una CPU da PC dovrebbe essere
eseguito in meno di 20 cicli di clock per byte generato, nel caso di richieste di notevoli
dimensioni. Fortuna può essere utilizzato anche per sostituire la maggior parte delle
funzioni di libreria PRNG.

9.5 L’accumulatore
L’accumulatore raccoglie dati casuali reali da varie sorgenti e li utilizza per rinnovare il
seme del generatore.

9.5.1 Sorgenti di entropia
Assumiamo che nell’ambiente vi siano diverse sorgenti di entropia, ognuna delle quali
può produrre eventi contenenti entropia in qualsiasi istante temporale. Non importa che
cosa si utilizzi esattamente come sorgente, purché vi sia almeno una sorgente che generi
dati imprevedibili per l’attaccante. Poiché non potete sapere in che modo sarà condotto
l’attacco, la scelta migliore è quella di trasformare in una sorgente di casualità qualsiasi
cosa che abbia l’aspetto di dati imprevedibili. Pressioni dei tasti e movimenti del mouse
consentono di ottenere sorgenti di casualità ragionevoli. Inoltre, dovreste aggiungere
un numero opportuno di sorgenti di temporizzazione. Potreste usare temporizzazioni
accurate di pressioni di tasti, movimenti e clic del mouse, risposte da parte di unità a disco
e stampanti, preferibilmente tutte insieme. Ancora, non è un problema se l’attaccante è
in grado di prevedere o copiare i dati provenienti da alcune delle sorgenti, purché non
sia in grado di farlo per tutte.
L’implementazione di sorgenti di casualità può richiedere parecchio lavoro. Generalmente
le sorgenti devono essere integrati nei vari driver hardware del sistema operativo, ed è
quasi impossibile farlo a livello utente.
Possiamo identificare ciascuna sorgente mediante un numero univoco compreso nell’intervallo da 0 a 255. Chi esegue l’implementazione può scegliere se allocare i numeri di
sorgente in modo statico o dinamico. I dati di ciascun evento sono costituiti da una breve

Generazione di dati casuali   137

sequenza di byte. Le sorgenti dovrebbero includere in ciascun evento soltanto i dati non
prevedibili. Per esempio, le informazioni di temporizzazione possono essere rappresentate
dai due o quattro byte meno significativi di un timer preciso. Non ha senso includere il
giorno, il mese e l’anno, perché si può presupporre che l’attaccante li conosca.
Concateneremo vari eventi provenienti da sorgenti diverse. Per garantire che una stringa
costruita da tale concatenazione codifichi gli eventi in modo univoco, dobbiamo assicurarci che la stringa sia analizzabile. Ciascun evento è codificato con tre o più byte di dati;
il primo byte contiene il numero di sorgente di casualità, il secondo contiene il numero
di byte di dati aggiuntivi e i byte successivi contengono i dati forniti dalla sorgente.
Naturalmente l’attaccante conoscerà gli eventi generati da alcune delle sorgenti. Per
creare un modello di questa situazione, assumiamo che alcune delle sorgenti siano interamente sotto il controllo dell’attaccante, che sceglie quali eventi generano e in quali
istanti. E come ogni altro utente, l’attaccante può chiedere dati casuali al PRNG in
qualsiasi momento.

9.5.2 Pool
Per rinnovare il seme del generatore, dobbiamo raccogliere gli eventi in un pool grande
a sufficienza perché l’attaccante non possa più enumerare i valori possibili per gli eventi
in questione. In questo modo si distruggono le informazioni che l’attaccante potrebbe
aver avuto sullo stato del generatore. Sfortunatamente non sappiamo quanti eventi raccogliere in un pool prima di utilizzarlo per rinnovare il seme del generatore. Questo è
il problema che Yarrow tenta di risolvere utilizzando stimatori di entropia e varie regole
euristiche. Fortuna lo risolve in un modo molto migliore.
Ci sono 32 pool: P0, P1, ... , P31. Ogni pool concettualmente contiene una stringa di
byte di lunghezza illimitata. In pratica, tale stringa è utilizzata soltanto come input di una
funzione di hash. Non è necessario che le implementazioni memorizzino la stringa di
lunghezza illimitata, possono calcolarne l’hash in modo incrementale mentre la stringa
viene assemblata nel pool.
Ogni sorgente distribuisce i propri eventi casuali sui vari pool in modo ciclico. Ciò garantisce che l’entropia di ciascuna sorgente sia distribuita sui pool in modo più o meno
uniforme. Ogni evento casuale è aggiunto alla stringa nel pool in questione.
Rinnoviamo il seme del generatore ogni volta che il pool P0 è sufficientemente lungo. Le
operazioni di rinnovo del seme sono numerate 1, 2, 3, ... . In base al numero di rinnovo
r, si include uno o più pool. Il pool Pi è incluso se 2i è un divisore di r. Quindi, P0 è utilizzato a ogni rinnovo, P1 ogni due rinnovi, P2 ogni quattro e così via. Dopo che un pool
è stato usato in un rinnovo del seme, viene azzerato impostandolo alla stringa vuota.
Questo sistema si adatta automaticamente alla situazione. Se l’attaccante ha poche informazioni sulle sorgenti di casualità, non sarà in grado di prevedere P0 al successivo
rinnovo del seme. Se invece ha molte più informazioni, potrebbe (falsamente) generare
un gran numero degli eventi. In tal caso, probabilmente l’attaccante sa abbastanza di P0
per essere in grado di ricostruire il nuovo stato del generatore a partire dal precedente e
dagli output. Ma quando viene utilizzato P1, contiene una quantità doppia di dati sconosciuti all’attaccanti, e P2 ne conterrà quattro volte tanti. A prescindere dal numero di
falsi eventi casuali che l’attaccante generi, o da quanti eventi conosca, finché vi è almeno
una sorgente di eventi casuali che l’attaccante non è in grado di prevedere, ci sarà sempre
un pool contenente una quantità di entropia sufficiente per sconfiggerlo.

138   Capitolo 9

La velocità con cui il sistema è in grado di recuperare da uno stato compromesso dipende
dal tasso a cui l’entropia (rispetto all’attaccante) entra nei pool. Se supponiamo che questo
sia un tasso fisso , allora dopo t secondi abbiamo in totale t bit di entropia. Ciascun
pool riceve circa t/32 bit in questo periodo di tempo. L’attaccante non può più tenere
traccia dello stato se il seme del generatore viene rinnovato con un pool contenente
più di 128 bit di entropia. Ci sono due casi. Se P0 contiene 128 bit di entropia prima
della successiva operazione di rinnovo del seme, allora è stato effettuato il recupero. La
velocità con cui ciò accade dipende dalla dimensione che lasciamo assumere a P0 prima
di rinnovare il seme. Il secondo caso si ha quando P0 effettua il rinnovo del seme troppo
velocemente, a causa di eventi casuali noti all’attaccante (o generati da esso). Sia t il tempo
tra un rinnovo del seme e il successivo. Allora il pool Pi contiene 2it/32 bit di entropia
tra un rinnovo e il successivo ed è utilizzato in un rinnovo ogni 2it secondi. Il recupero
avviene la prima volta che effettuiamo il rinnovo del seme con il pool Pi, dove 128 ≤
2it/32 < 256 (il limite superiore deriva dal fatto che altrimenti il pool Pi–1 conterrebbe
128 bit di entropia tra un rinnovo e l’altro). Questa diseguaglianza ci dà.

2i r t
< 256
32
e quindi:

892
r
In altre parole, il tempo tra i punti di recupero (2it) è limitato dal tempo necessario per
raccogliere 213 bit di entropia (8192/). Il numero 213 sembra grande, ma si può spiegare
come segue: ci servono almeno 128 = 27 bit per recuperare da una compromissione
del sistema. Nel caso più sfortunato il sistema esegue il rinnovo del seme subito prima
che abbiamo raccolto 27 bit in un particolare pool e dobbiamo quindi utilizzare il pool
successivo, che conterrà quasi 28 bit prima del rinnovo del seme. Infine, dividiamo i dati
su 32 pool, e questo porta a un altro fattore di 25.
Il risultato è molto buono. Questa soluzione differisce per un fattore di 64 da una soluzione ideale (richiede al più 64 volte la casualità che una soluzione ideale richiederebbe).
Questo è un fattore costante e ciò ci assicura che alla fine otterremo sempre il recupero.
Inoltre, non è necessario che conosciamo la quantità di entropia dei nostri eventi o quanto
sa l’attaccante. Questo è il reale vantaggio di Fortuna rispetto a Yarrow. Gli stimatori di
entropia, impossibili da realizzare, sono acqua passata. Tutto è interamente automatico.
Se c’è un buon flusso di dati casuali, il PRNG arriverà rapidamente al recupero, mentre
se il flusso di dati casuali è ridotto, servirà più tempo.
Finora abbiamo ignorato il fatto che abbiamo soltanto 32 pool e che forse anche il pool
P31 non contiene sufficiente casualità tra un rinnovo e l’altro per consentire il recupero.
Questo potrebbe accadere se l’attaccante ha inserito talmente tanti eventi casuali falsi che
si verificherebbero 232 rinnovi del seme prima che le sorgenti di casualità di cui l’attaccante non sa nulla generino 213 bit di entropia. È un caso improbabile, ma per impedire
all’attaccante di provarci, limiteremo la velocità dei rinnovi del seme. Un rinnovo sarà
eseguito soltanto se il rinnovo precedente è avvenuto più di 100 ms prima. Così si limita
il tasso di rinnovi a 10 per secondo, quindi serviranno più di 13 anni prima che si arrivi
a utilizzare P32. Data che la vita economica e tecnica della maggior parte dei computer
è molto inferiore a dieci anni, sembra ragionevole limitarci a 32 pool.
2i t <

Generazione di dati casuali   139

9.5.3 Considerazioni sull’implementazione
Nel progetto dell’accumulatore occorre tenere presenti alcune considerazioni sull’implementazione.

9.5.3.1 Distribuzione degli eventi sui pool
Gli eventi in arrivo devono essere distribuiti sui pool. La soluzione più semplice sarebbe
quella di affidare il compito all’accumulatore, ma è una soluzione pericolosa. Ci sarà
una chiamata di funzione che passi un evento all’accumulatore; è possibile che anche
l’attaccante possa effettuare chiamate arbitrarie di questa funzione ogni volta che un
evento “reale” è stato generato, influenzando così il pool in cui il successivo evento
“reale” sarebbe destinato. Se l’attaccante fa in modo di inviare tutti gli eventi “reali”
nel pool P0, il sistema multi-pool diventa inefficace e si applicano i metodi di attacco
per sistemi a pool singolo. Se l’attaccante fa inserire tutti gli eventi “reali” in P31, essi in
sostanza non saranno mai usati.
La nostra soluzione è di consentire a ciascun generatore di eventi di passare, con ogni
evento, il numero di pool appropriato. In questo modo l’attaccante deve poter accedere
alla memoria del programma che genera l’evento, se vuole influenzare la scelta del pool.
Ma se l’attaccante dispone di tale accesso, probabilmente sono compromesse tutte le
sorgenti.
L’accumulatore potrebbe controllare che ciascuna sorgente indirizzi i propri eventi ai
pool nell’ordine corretto. È utile che una funzione controlli che i dati di input siano ben
formati, perciò questa strategia in linea di principio è buona, ma nella nostra situazione
non è sempre chiaro che cosa dovrebbe fare l’accumulatore qualora il controllo fallisca.
Se l’intero PRNG è eseguito come processo utente, potrebbe sollevare un errore fatale e
causare l’uscita dal programma. Così si priverebbe il sistema del PRNG soltanto perché
una singola sorgente si è comportata in modo errato. Se il PRNG è parte del kernel del
sistema operativo, le cose si fanno molto più difficili. Supponiamo che un particolare
driver generi eventi casuali, ma non sia in grado di tenere traccia di un semplice contatore ciclico a 5 bit. Che cosa dovrebbe fare l’accumulatore? Restituire un codice di
errore? Un programmatore che commetta simili errori probabilmente non controllerà i
codici di ritorno. L’accumulatore dovrebbe arrestare il kernel? Sarebbe un po’ drastico,
si causerebbe il blocco dell’intera macchina a causa di un unico driver con problemi.
L’idea migliore a nostro parere è quella di penalizzare il driver riguardo il tempo della
CPU. Se la verifica fallisce, l’accumulatore potrebbe ritardare il driver di un secondo,
per esempio.
Questa idea non è poi così utile, dato che il motivo per cui lasciamo che il chiamante
determini il numero di pool è che supponiamo che l’attaccante possa effettuare false
chiamate all’accumulatore con eventi finti. Se accade ciò e l’accumulatore controlla
l’ordinamento dei pool, il generatore di eventi reale sarà penalizzato per l’errato comportamento dell’attaccante. La conclusione è questa: l’accumulatore non dovrebbe controllare l’ordinamento dei pool, perché non può fare nulla di utile nel caso in cui rilevi
un errore. Ogni sorgente di casualità ha la responsabilità di distribuire i propri eventi in
ordine ciclico sui pool. Se una sorgente sbaglia, potremmo perdere la sua entropia (cosa
prevista), ma non ci saranno altri danni.

140   Capitolo 9

9.5.3.2 Tempo di esecuzione del passaggio di eventi
Vogliamo limitare la quantità di calcoli necessari per passare un evento all’accumulatore.
Molti eventi sono di temporizzazione e vengono generati da driver in tempo reale,
che non possono sostenere un ritardo causato da una chiamata a un accumulatore che
richiede troppo tempo.
Esiste certamente un numero minimo di calcoli che è necessario eseguire. Dobbiamo
aggiungere i dati dell’evento al pool selezionato. Naturalmente non registreremo in
memoria l’intera stringa del pool, perché la sua lunghezza è illimitata. Ricordate che
le funzioni di hash popolari sono iterative? Per ogni pool avremo un piccolo buffer e
calcoleremo un hash parziale non appena il buffer è pieno. Questa è la minima quantità
di calcoli richiesti per evento.
Non vogliamo eseguire l’intera operazione di rinnovo del seme, che utilizza uno o più
pool per rinnovare il seme del generatore. Questo infatti richiede un tempo di un ordine
di grandezza maggiore rispetto alla semplice aggiunta di un evento a un pool. Questo
lavoro, invece, sarà rinviato fino a quando il successivo utente chiederà dati casuali, e
allora sarà svolto prima della generazione di tali dati. In questo modo parte del carico
computazionale viene spostato dai generatori di eventi agli utenti che chiedono i dati
casuali, cosa ragionevole perché sono gli utenti che traggono beneficio dal servizio del
PRNG. Dopo tutto, la maggior parte dei generatori di eventi non trae beneficio dai
dati casuali che aiuta a produrre.
Per consentire di eseguire il rinnovo del seme appena prima dell’elaborazione della richiesta di dati casuali, dobbiamo incapsulare il generatore. In altre parole, il generatore sarà
nascosto in modo che non possa essere richiamato direttamente. L’accumulatore fornirà
una funzione RandomData con la stessa interfaccia di PseudoRandomData. Così si
evitano problemi causati da alcuni utenti che richiamano il generatore direttamente e
bypassano il processo di rinnovo del seme su cui abbiamo tanto lavorato per perfezionarlo. Naturalmente gli utenti possono sempre creare la propria istanza del generatore
per utilizzarla come preferiscono.
Una tipica funzione di hash, come SHA-256, e quindi SHAd-256, elabora i messaggi
di input in blocchi di dimensione fissa. Se elaboriamo ciascun blocco della stringa del
pool non appena è completo, ogni evento porterà al più a un unico calcolo del blocco
di hash.Tuttavia, c’è anche uno svantaggio: i computer moderni utilizzano una gerarchia
di cache per mantenere impegnata la CPU. Uno dell’effetto delle cache è che risulta più
efficiente fare in modo che la CPU continui a lavorare sullo stesso compito per un po’
di tempo. Se elaborate un singolo blocco di codice hash, la CPU deve leggere il codice
della funzione di hash caricandolo nella cache più veloce prima di poterlo eseguire. Se
elaborate diversi blocchi in sequenza, il primo porta il codice nella cache più veloce,
e i successivi ne trarranno vantaggio. In generale, le prestazioni delle CPU moderne si
possono migliorare significativamente facendo in modo che la CPU continui a lavorare
in un piccolo ciclo ed evitando che passi continuamente da un codice all’altro.
Considerando quanto appena esposto, una possibilità è quella di aumentare la dimensione
del buffer per pool e raccogliere più dati in ciascun buffer prima di calcolare l’hash. Il
vantaggio è una riduzione del tempo della CPU richiesto. Lo svantaggio è che il tempo massimo richiesto per aggiungere un nuovo evento al pool aumenta. Si tratta di un
compromesso di implementazione che non possiamo risolvere qui, dipende troppo dai
dettagli dell’ambiente.

Generazione di dati casuali   141

9.5.4 Inizializzazione
L’inizializzazione è una funzione semplice. Finora abbiamo parlato soltanto del generatore
e dell’accumulatore, ma le funzioni che definiremo nel seguito fanno parte dell’interfaccia
esterna di Fortuna. I loro nomi riflettono il fatto che operano sull’interno PRNG.
function InitializePRNG
output: R Stato PRNG.

Imposta i 32 pool alla stringa vuota.

for i = 0, ... , 31 do

Pi ← ò

od

Imposta il contatore di rinnovi del seme a zero.

ReseedCnt ← 0

E inizializza il generatore.

G ← InitializeGenerator()

Impacchetta lo stato.

R ← (G, ReseedCnt, P0, ... , P31)

return R

9.5.5 Come ottenere dati casuali
Questo non è un semplice wrapper attorno al componente generatore del PRNG, perché
dobbiamo gestire le operazioni di rinnovo del seme.
function RandomData
input: R Stato del PRNG, modificato da questa funzione.

n Numero di byte di dati casuali da generare.
output: r Stringa pseudocasuale di byte.

if length(P0) ≥ MinPoolSize ^ ultimo rinnovo del seme > 100 ms fa then

Dobbiamo eseguire il rinnovo del seme.

ReseedCnt ← ReseedCnt + 1

Aggiunge gli hash di tutti i pool che utilizzeremo.

s←ò

for i ∈ 0, ... , 31 do
if 2i| ReseedCnt then
s ← s || SHAd-256(Pi)
Pi ← ò
fi

od

Ha ottenuto i dati, ora esegue il rinnovo del seme.

Reseed(G, s)

142   Capitolo 9


fi

if ReseedCnt = 0 then

Genera errore, PRNG non ha ancora ricevuto il seme

else

Esegue il rinnovo del seme (se necessari). Il lavoro è svolto dal generatore che fa parte
di R.

return PseudoRandomData(G, n)

fi
Questa funzione inizia controllando la dimensione del pool P0 rispetto al parametro
MinPoolSize per verificare se deve eseguire un rinnovo del seme. Potete utilizzare una
stima molto ottimistica sulla dimensione che il pool deve avere per poter contenere 128
bit di entropia. Assumendo che ciascun evento contenga 8 bit di entropia e che occupi 4
byte nel pool (corrispondenti a 2 byte di dati evento), un valore adatto per MinPoolSize
sarebbe 64 byte. Non conta molto, anche se pare sconsigliabile scegliere un valore minore
di 32. Anche scegliere un valore molto più grande non va bene, perché così si ritarderà
il rinnovo del seme anche ove vi siano buone sorgenti di casualità.
Il passo successivo è quello di incrementare il contatore di rinnovi. Il conteggio è stato
inizializzato a 0, perciò il primo rinnovo del seme utilizza il valore 1. Questo garantisce automaticamente che il primo rinnovo utilizzi soltanto P0, esattamente ciò che vogliamo.
Il ciclo aggiunge gli hash dei pool. Avremmo anche potuto aggiungere i pool stessi, ma
così ogni implementazione avrebbe dovuto memorizzare le stringhe intere dei pool, e
non solo il calcolo dell’hash corrispondente. La notazione 2i|ReseedCnt indica un test
di divisibilità: se è vero che 2i è un divisore del valore ReseedCnt. Se un valore i non
soddisfa questo test, falliranno anche i test di tutte le iterazioni successive, il che suggerisce
la necessità di un’ottimizzazione.

9.5.6 Aggiunta di un evento
Le sorgenti di casualità richiamano questa routine quando hanno un altro evento casuale. Notate che le sorgenti di casualità sono ognuna identificata da un numero univoco.
Non specificheremo come allocare questi numeri, perché la soluzione dipende dalla
situazione locale.
function AddRandomEvent
input: R Stato PRNG, modificato da questa funzione.

s Numero di sorgente compreso nell’intervallo 0, ... , 255.

i Numero di pool compreso nell’intervallo 0, ... , 31. Ogni sorgente deve
distribuire i propri eventi su tutti i pool in modalità round-robin.

e Dati dell’evento. Stringa di byte con lunghezza compresa nell’intervallo 1,
... , 32.

Prima controlla i parametri.

assert 1 ≤ length(e) ≤ 32 ^ 0 ≤ s ≤ 255 ^ 0 ≤ i ≤ 31

Aggiunge i dati al pool.

Pi ← Pi || s || length(e) || e

Generazione di dati casuali   143

L’evento è codificato in 2 + length(e) byte, dove s e length(e) sono entrambi codificati
come un singolo byte. Questa concatenazione viene poi aggiunta al pool. Notate che
le nostre specifiche si limitano ad aggiungere dati al pool, senza menzionare calcoli
di hash. Specifichiamo l’hashing del pool soltanto nel momento in cui lo utilizziamo.
Un’implementazione reale dovrebbe calcolare gli hash al volo. Questa soluzione è funzionalmente equivalente e più facile da implementare, ma una specifica diretta sarebbe
molto più complicata.
Abbiamo limitato la lunghezza dei dati dell’evento a 32 byte. Eventi più grandi sono
praticamente inutili; le sorgenti di casualità non devono passare grandi quantità di dati,
ma limitarsi ai pochi byte contenenti dati casuali non prevedibili. Se una sorgente ha una
grande quantità di dati che contengono una certa quantità di entropia diffusa in essa,
dovrebbe prima eseguire l’hashing dei dati. La funzione AddRandomEvent dovrebbe
sempre restituire il controllo velocemente. Questo è particolarmente importante perché
molte sorgente, per loro natura, svolgono servizi in tempo reale e non possono spendere
troppo tempo nella chiamata di AddRandomEvent. Se una sorgente produce piccoli
eventi, non dovrebbe avere la necessità di attendere altri chiamanti con eventi grandi.
La maggior parte delle implementazioni dovrà serializzare le chiamate di AddRandomEvent utilizzando un mutex per assicurarsi che sia aggiunto un solo evento per volta
(in un ambiente multithreaded è necessario prestare molta attenzione ad assicurarsi che
thread diversi non interferiscano l’uno con l’altro).
Alcune sorgenti di casualità potrebbero non avere il tempo di richiamare AddRandomEvent. In questo caso potrebbe essere necessario memorizzare gli eventi in un buffer e
fare in modo che un processo separato li estragga da lì e li inserisca nell’accumulatore.
Un’architettura alternativa consente alle sorgenti di passare semplicemente gli eventi
al processo accumulatore, dove un thread separato esegue tutti i calcoli di hash. È un
progetto più complesso, ma ha dei vantaggi per le sorgenti di entropia. La scelta dipende
in gran parte dalla situazione effettiva.

9.6 Gestione del file di seme
Il nostro PRNG finora raccoglie entropia e genera dati casuali dopo il primo rinnovo
del seme. Tuttavia, se riavviamo una macchina dobbiamo attendere che le sorgenti di
casualità producano eventi sufficienti per far scattare il primo rinnovo del seme, per
avere la disponibilità di dati casuali. In più, non vi è garanzia che lo stato dopo il primo
rinnovo del seme sia davvero impossibile da prevedere per l’attaccante.
La soluzione consiste nell’utilizzare un file di seme. Il PRNG mantiene un file separato pieno di entropia, chiamato appunto file di seme, non accessibile ad altri. Dopo un
riavvio della macchina, il PRNG legge il file di seme e lo utilizza come entropia per
raggiungere uno stato sconosciuto. Naturalmente, una volta che il file di seme è stato
utilizzato in questo modo, è necessario riscriverlo con nuovi dati.
Nel seguito descriviamo la gestione del file di seme, prima sotto l’ipotesi che il file system
supporti operazioni atomiche, e poi discutendo gli aspetti relativi all’implementazione
della gestione del file di seme su sistemi reali.

144   Capitolo 9

9.6.1 Scrittura di file di seme
La prima cosa da fare è generare un file di seme; si utilizza una semplice funzione.
function WriteSeedFile
input: R Stato del PRNG, modificato da questa funzione.

f File su cui scrivere.

write(f, RandomData(R, 64))
Questa funzione genera semplicemente 64 byte di dati casuali e li scrive sul file. I dati sono
leggermente più del minimo necessario, ma non è il caso di fare i parsimoniosi qui.

9.6.2 Aggiornamento di file di seme
Naturalmente dobbiamo essere in grado di leggere un file di seme. Per motivi spiegati
in seguito, nella stessa operazione effettuiamo anche l’aggiornamento del file.
function UpdateSeedFile
input: R Stato del PRNG, modificato da questa funzione.

f File da aggiornare.

s ← read(f )

assert length(s) = 64

Reseed(G, s)

write(f, RandomData(R, 64))
Questa funzione legge il file di seme, ne controlla la lunghezza e rinnova il seme del
generatore, poi riscrive il file di seme con nuovi dati casuali.
Questa routine deve assicurarsi che il PRNG non sia utilizzato nel periodo tra il rinnovo del seme e la scrittura dei nuovi dati sul file di seme. Ecco il problema: dopo un
riavvio, il file di seme viene letto da questa funzione e i dati vengono utilizzati per il
rinnovo del seme. Supponete che l’attaccante effettui una richiesta di dati casuali prima
che il file di seme sia aggiornato. Non appena questi dati vengono restituiti, ma prima
dell’aggiornamento del file di seme, l’attaccante riavvia la macchina. Al successivo avvio,
viene letto il file di seme contenente ancora gli stessi dati, che vengono utilizzati per il
rinnovo del seme del generatore. Questa volta, un utente innocente effettua una richiesta
di dati casuali prima che il file di seme sia riscritto: ottiene gli stessi dati casuali ottenuti
precedentemente dall’attaccante. Così si viola la segretezza dei dati casuali. Poiché spesso
utilizziamo dati casuali per generare chiavi crittografiche, il problema è piuttosto serio.
L’implementazione deve garantire che il file di seme sia mantenuto segreto. Inoltre, tutti
gli aggiornamenti del file di seme devono essere atomici (cfr. Paragrafo 9.6.5).

9.6.3 Quando leggere e scrivere sul file di seme
Quando il computer viene riavviato, il PRNG non ha alcuna entropia da cui generare
dati casuali. Ecco a che cosa serve il file di seme, che deve quindi essere letto e aggiornato dopo ogni riavvio.

Generazione di dati casuali   145

Il computer, mentre è in funzione, raccoglie entropia da varie sorgenti, e vogliamo che
tale entropia vada a interessare anche il file di seme. Una soluzione ovvia è quella di
riscrivere il file di seme appena la macchina sta per essere spenta. Poiché alcuni computer non vengono mai spenti in modo ordinato, il PRNG dovrebbe inoltre riscrivere il
file di seme a intervalli regolari. Non entreremo nei dettagli, che sono poco interessanti
e dipendono spesso dalla piattaforma. È importante assicurarsi che il file di seme sia
aggiornato regolarmente dal PRNG dopo che è stata raccolta una quantità sufficiente
di entropia. Una soluzione ragionevole sarebbe quella di riscrivere il file di seme a ogni
spegnimento e a periodi regolari di 10 minuti.

9.6.4 Backup e macchine virtuali
Il tentativo di eseguire correttamente il rinnovo del seme apre una serie di problemi
intricati. Non possiamo consentire che lo stesso stato del PRNG sia ripetuto due volte, e
a questo scopo utilizziamo il file system per memorizzare un file di seme, ma la maggior
parte dei file system non è progettata per evitare la ripetizione di uno stesso stato, e questo causa parecchie difficoltà. In primo luogo, ci sono i backup. Se effettuate un backup
dell’intero file system e poi riavviate il computer, viene effettuato il rinnovo del seme
del PRNG utilizzando il file di seme. Se in seguito si effettua il ripristino dell’intero file
system dal backup e si riavvia il computer, verrà utilizzato lo stesso file di seme. In altre
parole, finché l’accumulatore non ha raccolto entropia sufficiente, il PRNG produrrà lo
stesso output dopo due riavvii. Questo è un problema serio, perché un attaccante potrebbe
sfruttarlo per recuperare i dati casuali che un altro utente ha ottenuto dal PRNG.
Non esiste una difesa diretta contro questo tipo di attacco. Se il sistema di backup è in
grado di ricreare l’intero stato permanente del computer, non possiamo fare nulla per
evitare che lo stato del PRNG si ripeta. Idealmente dovremmo correggere il sistema di
backup in modo che tenga conto del PRNG, ma sarebbe chiedere troppo. Effettuando
l’hash del file di seme con l’istante temporale corrente risolverebbe il problema, purché
l’attaccante non resetti il clock allo stesso istante. La stessa soluzione potrebbe essere
utilizzata se il sistema di backup mantiene un contatore del numero di operazioni di
ripristino effettuate: potremmo effettuare l’hash del file di seme con tale contatore.
Le macchine virtuali pongono un problema simile a quello dei backup. Se si salva lo stato
di una macchina virtuale e poi la si riavvia due volte, entrambe le istanze inizieranno con lo
stesso stato del PRNG. Fortunatamente, alcune delle soluzioni valide per i backup valgono
anche per una molteplicità di istanze di macchine virtuali avviate dallo stesso stato.
I problemi posti da backup e macchine virtuali meritano ulteriori studi, ma poiché per
la maggior parte dipendono dalla piattaforma, preferiamo non trattarli qui.

9.6.5 Atomicità degli aggiornamenti del file system
Un altro importante problema associato al file di seme è l’atomicità degli aggiornamenti
del file system. Sulla maggior parte dei sistemi operativi, se si scrive un file di seme, tutto
ciò che accade è l’aggiornamento di alcuni buffer di memoria, mentre i dati vengono
scritti effettivamente su disco soltanto in seguito. Alcuni file system hanno un comando
di “flush” che indica esplicitamente di eseguire la scrittura di tutti i dati su disco; tuttavia,
questa operazione può essere estremamente lenta, abbiamo visto casi in cui l’hardware
si è addirittura rifiutato di implementare in modo appropriato tale comando.

146   Capitolo 9

Ogni volta che effettuiamo il rinnovo del seme dal file di seme, dobbiamo aggiornare
quest’ultimo prima di consentire a qualsiasi utente di chiedere dati casuali. In altre parole,
dobbiamo essere assolutamente certi che i dati siano stati modificati sul disco magnetico.
Le cose si fanno ancora più complicate quando si pensa che molti file system trattano
separatamente i dati dei file e le informazioni per l’amministrazione dei file. Perciò, la
riscrittura del file di seme potrebbe rendere i dati di amministrazione del file temporaneamente incoerenti. Se durante tale operazione si interrompe l’alimentazione elettrica,
si rischia di ottenere un file di seme danneggiato o anche di perderlo del tutto, cosa assai
problematica per un sistema di sicurezza.
Alcuni file system utilizzano il journaling per risolvere alcuni di questi problemi. Si tratta di una tecnica sviluppata in origine per i grandi sistemi di database. Il journal è un
elenco sequenziale di tutti gli aggiornamenti effettuati al file system; se è utilizzato in
modo appropriato, questo strumento può garantire che gli aggiornamenti siano sempre
coerenti. Un file system dotato di questa funzionalità è sempre preferibile dal punto di
vista dell’affidabilità; purtroppo, alcuni dei file system più comuni applicano il journaling
soltanto alle informazioni di amministrazione, cosa insufficiente per i nostri scopi.
Se l’hardware e il sistema operativo non supportano aggiornamenti di file interamente
atomici e permanenti, non possiamo realizzare una soluzione di file di seme perfetta. In
questo caso occorre esaminare con cura la piattaforma su cui si lavora e fare del proprio
meglio per aggiornare in modo affidabile il file di seme.

9.6.6 Il primo avvio
Quando si avvia il PRNG per la prima volta, non c’è un file di seme da utilizzare. Per
esempio, considerate un nuovo PC venduto con sistema operativo preinstallato. Il sistema
genera alcune chiavi crittografiche di amministrazione per l’installazione, per cui ha bisogno del PRNG. Per facilitare la produzione, tutte le macchine sono identiche e hanno
dati identici. Non c’è un file di seme iniziale, perciò non posiamo usarlo. Potremmo
attendere che una quantità sufficiente di eventi casuali faccia scattare uno o più rinnovi
del seme, ma servirebbe parecchio tempo, e inoltre non sapremo mai quando avremo
raccolto entropia sufficiente per poter generare buone chiavi crittografiche.
Una buona idea sarebbe quella di fare in modo che la procedura di installazione generi
un file di seme casuale per il PRNG durante la configurazione. Per esempio, si potrebbe
utilizzare un PRNG che si trova su un computer separato per generare un nuovo file
di seme per ciascuna macchina. Oppure, il software di installazione potrebbe chiedere
all’utente di muovere il mouse per raccogliere una quantità iniziale di entropia. La scelta
della soluzione dipende dai dettagli dell’ambiente, ma in qualche modo è necessario
fornire inizialmente dell’entropia, non c’è alternativa. L’accumulatore di entropia può
impiegare un certo tempo per fornire il seme appropriato al PRNG, ed è abbastanza
probabile che alcune chiavi crittografiche molto importanti saranno generate da quest’ultimo poco dopo l’installazione della macchina.
Tenete presente che l’accumulatore di Fortuna fornisce il seme al generatore non appena
dispone di entropia sufficiente a garantire un dato realmente casuale. In base alla quantità
di entropia fornita dalle sorgenti, un elemento che Fortuna non può conoscere, potrebbe
passare un certo tempo prima di raccogliere entropia sufficiente per rinnovare il seme
del generatore. Disporre di una sorgente esterna di casualità per creare il primo file di
seme è probabilmente la migliore soluzione.

Generazione di dati casuali   147

9.7 Scelta di elementi casuali
Il nostro PRNG produce sequenze di byte casuali. A volte è proprio questo ciò che
serve, mentre in altre situazioni si vuole scegliere un elemento casuale da un insieme;
nel secondo caso serve qualche accortezza in più.
Ogni volta che scegliamo un elemento casuale, assumiamo implicitamente che sia scelto
a caso, secondo una distribuzione uniforme, da un insieme specificato (a meno che non
sia indicata un’altra distribuzione di probabilità). Ciò significa che ciascun elemento
dovrebbe avere esattamente la stessa probabilità di essere scelto (se lavoriamo a un livello
di sicurezza a 128 bit, potremmo sostenere una deviazione dalla probabilità uniforme di
2−128, ma è più facile procedere con esattezza).
Sia n il numero di elementi dell’insieme da cui vogliamo scegliere. Mostreremo soltanto
come scegliere un elemento a caso dall’insieme 0, 1, ... , n − 1, perché una volta in grado
di fare ciò, è facile scegliere elementi da qualsiasi insieme di dimensione n.
Se n = 0 non ci sono elementi tra cui scegliere, perciò si tratta di un caso di errore. Se
n = 1 non c’è scelta, è un altro caso semplice. Se n = 2k, allora si ottengono k bit di
dati casuali dal PRNG e si interpretano come numero compreso nell’intervallo 0, ... ,
n − 1. Questo numero è uniformemente casuale (potrebbe essere necessario ottenere
un numero intero di byte dal PRNG e scartare alcuni bit dell’ultimo byte per ottenere
k bit, ma non è difficile).
E se n non è una potenza di 2? Alcuni programmi scelgono un intero casuale a 32 bit
e lo riducono modulo n, ma tale algoritmo introduce un difetto nella distribuzione di
probabilità risultante. Prendiamo n = 5 come esempio e definiamo m := 232/5. ��������
Se prendiamo un numero uniformemente casuale a 32 bit e lo riduciamo modulo 5, i risultati
1, 2, 3 e 4 si verificano ciascuno con una probabilità di m/232, mentre il risultato 0 si
verifica con probabilità (m + 1)/232. La deviazione è piccola, ma potrebbe essere significativa. Sarebbe certamente facile individuare la deviazione con meno dei 2128 passaggi
consentiti all’attaccante.
Il modo corretto per scegliere un numero casuale in un intervallo arbitrario è quello di
utilizzare un approccio per tentativi. Per generare un valore casuale compreso nell’intervallo 0, ... , 4, prima generiamo un valore casuale compreso nell’intervallo 0, ... , 7,
cosa che possiamo fare perché 8 è una potenza di 2. Se il risultato è maggiore o uguale
a 5, lo scartiamo e scegliamo un nuovo numero casuale compreso nell’intervallo 0, ... , 7;
continuiamo finché il risultato rientra nell’intervallo desiderato. In altre parole, generiamo
un numero casuale contenente il giusto numero di bit e scartiamo tutti quelli che non
soddisfano tale condizione.
Riportiamo una specifica più formale di come scegliere un numero casuale nell’intervallo n − 1 per n ≥ 2.
1. Sia k il più piccolo intero tale che 2k ≥ n.
2. Utilizzare il PRNG per generare un numero casuale a k bit K. Questo numero
sarà compreso nell’intervallo 0, ... , 2k − 1. Potrebbe essere necessario generare un
numero intero di byte e scartare parte dell’ultimo, ma è facile farlo.
3. Se K ≥ n tornare al punto 2.
4. Il numero K è il risultato.
Questa procedura potrebbe comportare qualche spreco. Nel caso peggiore, scartiamo in
media metà dei tentativi. Ecco una versione migliorata: poiché 232 − 1 è un multiplo di 5

148   Capitolo 9

potremmo scegliere un numero casuale compreso nell’intervallo 0, ... , 232 − 2 e ridurre
il risultato 5 per ottenere quanto desiderato. Per scegliere un valore numero compreso
nell’intervallo 0, ... , 232 − 2, utilizziamo l’algoritmo “inefficiente” che procede per tentativi
e scarti, ma ora la probabilità di dover scartare il risultato intermedio è molto bassa.
La formulazione generale è la seguente: scegliere un k tale che 2k ≥ n. Definire q :=
2k/n. Scegliere un numero casuale r compreso nell’intervallo 0, ... , nq − 1 utilizzando la
procedura per tentativi e scarti. Una volta generato un r adatto, il risultato finale è dato
da (r mod n).
Non conosciamo alcun modo di generare numeri uniformemente casuali su dimensioni
che non siano potenze di due senza dover scartare alcuni bit casuali, ma questo non è
un problema. Con un PRNG decente, non vi è scarsità di bit casuali.

9.8 Esercizi
Esercizio 9.1 Esaminate i generatori di numeri casuali integrati in tre dei linguaggi
di programmazione che preferite. Li utilizzereste per scopi di crittografia?
Esercizio 9.2 Utilizzando una libreria crittografica esistente, scrivete un breve programma che generi una chiave AES a 256 bit utilizzando un PRNG crittografico.
Esercizio 9.3 Prendete come riferimento una piattaforma, un linguaggio e una libreria
crittografica e riassumete il funzionamento interno del PRNG crittografico. Considerate
anche aspetti come i seguenti: come viene ottenuta l’entropia, come avviene il rinnovo
del seme, come il PRNG gestisce il riavvio.
Esercizio 9.4 Quali sono i vantaggi dell’utilizzo di un PRNG rispetto a un RNG?
E quelli dell’utilizzo di un RNG rispetto a un PRNG?
Esercizio 9.5 Utilizzando un PRNG crittografico che fornisce un flusso di bit,
implementate un generatore di numeri casuali che fornisce interi casuali nell’intervallo
0, 1, ... , n − 1 per ogni n compreso tra 1 e 232.
Esercizio 9.6 Implementate un approccio intuitivo per generare numeri casuali
compresi nell’insieme 0, 1, ... , 191. A questo scopo, generate un valore casuale a 8 bit,
interpretatelo come intero e riducetelo modulo 192. Procedendo in modo sperimentale,
generate un’elevata quantità di numeri casuali nell’insieme 0, 1, ... , 191 e scrivete una
relazione sulla distribuzione dei risultati.
Esercizio 9.7 Trovate un nuovo prodotto o sistema che utilizza (o dovrebbe utilizzare)
un PRNG crittografico. Potrebbe anche essere lo stesso prodotto o sistema analizzato
nell’Esercizio 1.8. Eseguite una revisione della sicurezza per tale prodotto o sistema come
descritto nel Paragrafo 1.12, ma questa volta concentrandovi sugli aspetti legati all’uso
di numeri casuali.

Capitolo 10

I numeri primi

Questo capitolo e il seguente sono dedicati ai sistemi
di crittografia a chiave pubblica. Per cominciare sono
necessarie alcune nozioni matematiche. Si è sempre
tentati di limitarsi a mostrare formule ed equazioni
senza spiegare, ma è nostra forte convinzione che
questa sia una via pericolosa. Per utilizzare uno
strumento, è bene conoscerne le proprietà. Nel caso
di una funzione di hash è semplice: abbiamo un
modello “ideale” di funzione di hash e desideriamo
che la funzione reale si comporti come il modello
ideale. Le cose si fanno più difficili con i sistemi a
chiave pubblica perché non ci sono modelli “ideali”
con cui lavorare. In pratica si devono affrontare le
proprietà matematiche dei sistemi a chiave pubblica, e
per farlo in maniera sicura è necessario comprendere
queste proprietà. Non ci sono scorciatoie: occorre
apprendere le basi matematiche. Fortunatamente le
conoscenze di base richieste sono quelle fornite dalla
matematica della scuola superiore.
Questo capitolo è dedicato ai numeri primi, che giocano un ruolo importante nella matematica, ma a noi
interessano perché su di essi si basano alcuni dei più
importanti sistemi crittografici a chiave pubblica.

10.1 Divisibilità e numeri primi
Un numero a è un divisore di b (notazione a | b, si
legge “a è divisore di b”) se si può dividere b per a
senza che rimanga un resto. Per esempio, 7 è un divisore di 35, quindi si scrive 7 | 35. Un numero viene
chiamato numero primo se ha due e due soli divisori
positivi: 1 e se stesso. Per esempio, 13 è un numero

Sommario
10.1 Divisibilità e numeri
primi
10.2 Generazione di numeri
primi piccoli
10.3 Calcolo modulare con
numeri primi
10.4 Numeri primi grandi
10.5 Esercizi

150   Capitolo 10

primo; i due divisori sono 1 e 13. I primi numeri primi sono facili da trovare: 2, 3, 5,
7, 11, 13, ... Ogni intero maggiore di 1 che non è primo si dice composto. Il numero 1
non è né primo né composto.
In questo capitolo e nei successivi utilizzeremo la notazione e la terminologia matematica appropriata, in modo da facilitarvi nella lettura di altri libri su questa materia.
La notazione potrebbe sembrare difficile e complicata all’inizio, ma questa parte della
matematica è davvero semplice.
Ecco un semplice lemma sulla divisibilità:
Lemma 1

Se a | b e b | c allora a | c.

Dimostrazione. Se a | b, allora esiste un intero s tale che as = b (b è divisibile per a, quindi
deve essere multiplo di a). Se b | c, allora esiste un intero t tale che bt = c. Ma questo
implica che c = bt = (as)t = a(st), e di conseguenza a è un divisore di c. Per seguire questa
argomentazione, verificate che ogni segno di uguale sia corretto; la conclusione è che il
primo elemento c deve essere uguale all’ultimo elemento a(st).
o
Il lemma è la dichiarazione di un fatto. La dimostrazione prova che il lemma è vero.
Il piccolo riquadro segnala la fine della dimostrazione. Ai matematici piace utilizzare
molti simboli, ma questo presenta vantaggi e svantaggi; in questo libro utilizzeremo ciò
che riteniamo più appropriato. Questo è un lemma molto semplice e la dimostrazione
dovrebbe essere facile da seguire, se ricordate il significato della notazione a | b.
I numeri primi sono stati studiati dai matematici fin dall’antichità. Ancora oggi, se volete
generare tutti i numeri primi minori di un milione, dovreste utilizzare un algoritmo
sviluppato oltre 2000 anni fa da Eratostene, un amico di Archimede. Eratostene fu anche
la prima persona a calcolare accuratamente il diametro della terra. 1700 anni dopo sembra
che Colombo utilizzò una stima molto inferiore (e sbagliata) per la dimensione della
terra, quando pianificò la sua navigazione verso l’India andando verso ovest. Euclide, un
altro matematico greco, fornì un’elegante dimostrazione del fatto che esistono infiniti
numeri primi. È una dimostrazione talmente elegante che la riportiamo qui; leggerla vi
aiuterà a familiarizzare nuovamente con la matematica.
Prima di iniziare con la vera dimostrazione, riportiamo un semplice lemma.
Lemma 2
Sia n un numero positivo maggiore di 1. Sia d il minimo di n che è maggiore
di 1. Allora d è un numero primo.
Dimostrazione. Per prima cosa dobbiamo verificare che d sia ben definito (se esiste un
numero n che non ha un divisore minimo, allora d non è ben definito e il lemma non
ha senso). Sappiamo che n è un divisore di n, e che n > 1, quindi c’è almeno un divisore
di n che è maggiore di 1. Di conseguenza, deve anche esistere un divisore minimo che
sia maggiore di 1.
Per provare che d è primo, utilizziamo un trucco dei matematici chiamato dimostrazione per
assurdo. Per dimostrare un’affermazione X, supponiamo che X non sia vera e mostriamo
che questa ipotesi porta a una contraddizione. Se ipotizzare che X non sia vera porta a
una contraddizione, allora ovviamente X deve essere vera.
Nel nostro caso supponiamo che d non sia un numero primo. Se d non è un numero
primo, ha un divisore e tale che 1 < e < d. Ma dal Lemma 1 sappiamo che, se e | d e d
| n, allora e | n, quindi e è un divisore di n ed è minore di d. Ma questa è una contraddizione, perché d è stato definito come il minimo divisore di n. Quindi la nostra ipotesi
deve essere falsa, e di conseguenza d deve essere primo.
o

I numeri primi   151

Non preoccupatevi se questo tipo di dimostrazione vi crea un po’ di confusione: ci vuole
un po’ per abituarsi.
Ora possiamo dimostrare che esistono infiniti numeri primi.
Teorema 3 (Euclide)

Esistono infiniti numeri primi.

Dimostrazione. Supponiamo di nuovo che valga l’opposto di ciò che vogliamo dimostrare.
Questa volta ipotizziamo che il numero di numeri primi sia finito, e quindi che l’elenco
dei numeri primi sia finito. Chiamiamo questi numeri p1, p2, p3, ... , pk, dove k è il numero
di numero primi. Definiamo il numero n := p1 p2 p3 ··· pk + 1, che è il prodotto di tutti
i numeri primi più 1.
Consideriamo il minimo divisore di n che sia maggiore di 1; lo chiameremo di nuovo
d. Ora d è un numero primo (per il Lemma 2) e d | n. Ma nessuno dei numeri primi
del nostro elenco finito è un divisore di n. Infatti, sono tutti divisori di n − 1, quindi se
dividiamo n per uno dei numeri primi pi presenti nell’elenco, rimane sempre un resto pari
a 1. Quindi d è un numero primo e non è nell’elenco. Ma questa è una contraddizione,
poiché la lista dovrebbe contenere tutti i numeri primi. Quindi, l’ipotesi che il numero
di numeri primi sia finito porta a una contraddizione. Dobbiamo quindi concludere i
numeri primi sono infiniti.
o
Questa è fondamentalmente la dimostrazione che Euclide fornì oltre 2000 anni fa.
Esistono molti altri risultati che riguardano la distribuzione dei numeri primi, ma è interessante notare che non esiste alcuna formula semplice per calcolare il numero esatto di
numeri primi contenuti in uno specifico intervallo. I numeri primi sembrano distribuirsi
in maniera abbastanza casuale. Ci sono anche congetture molto semplici che non sono
state ancora provate. Per esempio, la congettura di Goldbach dice che ogni numero pari
maggiore di 2 è la somma di due numeri primi. Questo è semplice da verificare con
un computer per numeri relativamente piccoli, ma i matematici non sanno ancora se la
congettura sia valida per tutti i numeri pari.
È utile conoscere il teorema fondamentale dell’aritmetica: ogni intero maggiore di 1 può
essere scritto esattamente in un solo modo come prodotto di numeri primi (se non si
tiene conto dell’ordine in cui si scrivono i numeri primi). Per esempio, 15 = 3 · 5; 255
= 3 · 5 · 17; 60 = 2 · 2 · 3 · 5. Se vi interessano i dettagli, consultate qualsiasi testo di
teoria dei numeri; non riportiamo qui la dimostrazione.

10.2 Generazione di numeri primi piccoli
A volte è utile avere a disposizione un elenco di numeri primi piccoli. Ci viene in aiuto
il crivello di Eratostene, che è ancora il miglior algoritmo per generare numeri primi
piccoli. Il valore 220 nel pseudocodice riportato di seguito è una costante modificabile
in maniera appropriata.
function SmallPrimeList
input: n Limita i numeri primi da generare. Deve soddisfare la condizione 2≤n ≤ 220.
output: P L’elenco di tutti i numeri primi ≤ n.

Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

152   Capitolo 10


Occorre limitare la dimensione di n. Se n è troppo grande si potrebbe esaurire la memoria.

assert 2 ≤ n ≤ 220

Inizializza un elenco di flag tutti impostati a uno.

(b2 , b3 , ... , bn) ← (1, 1, ... , 1)

i←2

while i2 ≤ n do

Abbiamo trovato un numero primo i. Segnaliamo tutti i multipli di i come numeri
composti.

for j ∈ 2i, 3i, 4i, ... , n/i i do
bj ← 0

od

Cerchiamo il prossimo numero primo nel nostro elenco. Si può mostrare che questo ciclo
non arriva mai alla condizione i > n, che porterebbe all’elemento inesistente bi.

repeat
i ← i + 1

until bi = 1

od

Tutti i nostri numeri primi ora sono impostati a uno. Li raccogliamo in un elenco.

P ← []

for k ∈ 2, 3, 4, ... , n do

if bk = 1 then
P ← P || k

fi

od

return P
L’algoritmo è basato su un’idea semplice: qualsiasi numero composto c è divisibile per un
numero primo minore di c. Manteniamo un elenco di flag, uno per ogni numero fino a n.
Ogni flag indica se il numero corrispondente può essere un numero primo. Inizialmente
tutti i numeri sono contrassegnati come potenziali numeri primi, impostando il flag a 1.
Iniziamo con i che corrisponde al numero primo 2. Naturalmente nessuno dei multipli
di i può essere un numero primo, quindi indichiamo come composti i numeri 2i, 3i, 4i
e così via impostando il loro flag a 0. Incrementiamo quindi i fino a ottenere un altro
candidato a numero primo. Questo candidato non è divisibile per nessun numero primo
più piccolo, altrimenti sarebbe già stato segnato come composto. Quindi il nuovo i deve
essere il prossimo numero primo. Continuiamo a segnare i numeri composti e trovare
il successivo numero primo fino a che i2 > n.
È chiaro che nessun numero primo sarà mai contrassegnato come composto, visto che
segniamo come composto un numero solo quando ne conosciamo un fattore (il ciclo
che lo contrassegna come numero composto itera sui valori 2i, 3i, ... Ognuno di questi
termini ha un fattore i e quindi non può essere primo).

I numeri primi   153

Perché possiamo fermarci quando i2 > n? Supponiamo che un numero k sia composto,
e sia p il suo minimo divisore maggiore di 1. Sappiamo già che p è un numero primo
(cfr. il Lemma 2). Sia q := k/p. Ora abbiamo p ≤ q, altrimenti q sarebbe un divisore di k
minore di p, cosa che contraddice la definizione di p. L’osservazione cruciale è che
p ≤ k, perché se p fosse maggiore di k dovremmo avere k = p · q > k · q ≥ k · p >
k · k = k. Quest’ultima disuguaglianza mostrerebbe che k > k, cosa ovviamente falsa.
Quindi p ≤ k.
Abbiamo dimostrato che ogni numero composto k è divisibile per un numero primo ≤
k. Quindi ogni numero composto ≤ n è divisibile per un numero primo ≤ n. Quando
i2 > n, i > n. Ma abbiamo già contrassegnato i multipli di tutti i numeri primi minori
di i come composti nell’elenco, quindi tutti i composti < n sono già stati contrassegnati
in questo modo. I numeri dell’elenco che sono ancora contrassegnati come primi sono
davvero primi. La parte finale dell’algoritmo non fa che raccoglierli in un elenco da restituire. Si potrebbe ottimizzare questo algoritmo in molti modi, ma per semplicità non ce
ne occupiamo. Se implementato in maniera appropriata, l’algoritmo è molto veloce.
Vi chiederete perché abbiamo bisogno di numeri primi piccoli. La risposta è che i numeri
primi piccoli sono utili per generare numeri primi grandi; ci arriveremo presto.

10.3 Calcolo modulare con numeri primi
Il motivo principale per cui i numeri primi sono così utili per la crittografia è che si
possono utilizzare nel calcolo modulare.
Sia p un numero primo. Nel calcolo modulare con un numero primo usiamo solo i numeri 0, 1, ... , p − 1. La regola base per i calcoli modulari con un numero primo indica
di eseguire i calcoli utilizzando i numeri come interi, come si farebbe normalmente, ma
ogni volta che si ottiene un risultato r se ne calcola il modulo p. Dato un numero, calcolarne il modulo è facile: si divide r per p, si scarta il quoziente e si tiene il resto come
risultato. Per esempio, per calcolare 25 modulo 7 si divide 25 per 7, ottenendo quoziente
3 con resto 4. Il resto è il risultato, quindi (25 mod 7) = 4. La notazione (a mod b) è
utilizzata per indicare un’operazione modulo esplicita, ma i calcoli modulari sono usati
molto spesso, e sono utilizzate anche molte altre notazioni. Spesso l’intera equazione
sarà scritta senza alcuna operazione di modulo, e quindi (mod p) sarà aggiunto alla fine
dell’equazione per ricordare che il risultato si ottiene dopo aver calcolato il modulo
rispetto a p. Quando la situazione è chiara fin dal contesto, anche questo passaggio viene
tralasciato, e dovrete ricordarvi che il modulo è sottinteso.
Non occorre scrivere le parentesi attorno al calcolo di un modulo. Potremmo anche
scrivere a mod b, ma visto che l’operatore di modulo sembra molto simile a un testo
normale, questo potrebbe creare un po’ di confusione per chi non è abituato all’aritmetica
modulare. Per evitare confusioni scriveremo (a mod b) tra parentesi o a (mod b), in base
a quale scrittura risulti più chiara nello specifico contesto.
Tenete presente che il risultato di un’operazione di modulo p per qualsiasi intero è
compreso nell’intervallo 0, ... , p − 1, anche se l’intero originale è negativo. Alcuni linguaggi di programmazione permettono di ottenere risultati negativi da una operazione
di modulo (cosa molto irritante per i matematici). Se volete calcolare il modulo rispetto
a p di –1, il risultato è p – 1. Più in generale, per calcolare (a mod p) trovate gli interi q
e r tali che a = qp + r e 0 ≤ r < p. Il valore di (a mod p) è definito pari a r. Se impostate
a = −1 scoprirete che q = −1 e r = p − 1.

154   Capitolo 10

10.3.1 Addizione e sottrazione
L’addizione modulo p è facile: sommate i numeri e sottraete p se il risultato è maggiore
o uguale a p. Dato che entrambi i valori in ingresso sono compresi nell’intervallo 0, ... ,
p − 1, la somma non può essere maggiore di 2p − 1, quindi dovete sottrarre p al massimo
una volta per portare il risultato nell’intervallo appropriato.
La sottrazione è simile all’addizione: sottraete i numeri e poi aggiungete p se il risultato
è negativo.
Queste regole funzionano solo se i valori in ingresso sono entrambi valori modulo p. Se
sono al di fuori di questo intervallo, dovete effettuare una completa riduzione modulo p.
Per abituarsi ai calcoli modulari ci vuole un po’. Considerate l’espressione 5 + 3 = 1
(mod 7). A prima vista sembra strana. Sappiamo che 5 più 3 non fa 1, ma mentre 5 +
3 = 8 è vero per i numeri interi, lavorando in modulo 8 abbiamo 8 mod 7 = 1, quindi
5 + 3 = 1 (mod 7).
L’aritmetica modulare è utilizzata nella vita reale senza quasi che ce ne accorgiamo. Quando calcoliamo l’ora del giorno, prendiamo le ore modulo 12 (o modulo 24). Un orario
di un autobus potrebbe dichiarare che l’autobus parte 55 minuti dopo l’ora e impiega
15 minuti. Per scoprire quando arriva l’autobus, calcoliamo 55 + 15 = 10 (mod 60) e
determiniamo che arriva 10 minuti dopo l’ora. Per ora ci limitiamo al calcolo modulare
con numeri primi, ma si possono fare calcoli modulo qualsiasi numero.
Un punto importante da notare è che, se si ha un’espressione lunga come 5 + 2 + 5 +
4 + 6 (mod 7), si può inserire il modulo in qualsiasi punto.
Per esempio, si può sommare 5 + 2 + 5 + 4 + 6 e ottenere 22, poi calcolare 22 (mod
7) e ottenere 1. In alternativa si può calcolare 5 + 2 (mod 7) e ottenere 0, poi calcolare
0 + 5 (mod 7) e ottenere 5, poi 5 + 4 (mod 7) e ottenere 2, e poi 2 + 6 (mod 7) e
ottenere 1.

10.3.2 Moltiplicazione
La moltiplicazione, come sempre, comporta più lavoro dell’addizione. Per calcolare (ab
mod p), prima si calcola ab come intero, poi si prende il risultato modulo p. Il valore di
ab può essere grande come (p − 1)2 = p2 − 2p + 1, quindi occorre eseguire una divisione
con resto fino a trovare (q, r) tali che ab = qp + r e 0 ≤ r < p. Scartate q, r è il risultato.
Ecco un esempio: sia p = 5. Quando calcoliamo 3 · 4 (mod p) il risultato è 2, infatti 3 ·
4 = 12 e (12 mod 5) = 2. Quindi otteniamo 3 · 4 = 2 (mod p).
Come per l’addizione, si può calcolare il modulo una volta sola o iterativamente. Per
esempio, data un’espressione lunga come 3 · 4 · 2 · 3 (mod p), si può calcolare 3 · 4 · 2
· 3 = 72 e quindi calcolare (72 mod 5) = 2. Oppure si può calcolare (3 · 4 mod 5) = 2,
poi (2 · 2 mod 5) = 4 e poi (4 · 3 mod 5) = 2.

10.3.3 Gruppi e campi finiti
I matematici chiamano campo finito l’insieme dei numeri modulo un numero primo p
e lo indicano con “campo mod p” o semplicemente “mod p” (per una definizione più
rigorosa invitiamo a consultare un libro di matematica).

I numeri primi   155

Riportiamo alcuni aspetti che è utile ricordare sui calcoli in un campo mod p.
• Potete sempre aggiungere o sottrarre qualsiasi multiplo di p dai vostri numeri senza
cambiarne il risultato.
• Tutti i risultati sono compresi nell’intervallo 0, 1, ... , p − 1.
• Potete pensare di svolgere l’intero calcolo con gli interi e calcolare il modulo solo
alla fine. In questo modo tutte le regole algebriche apprese sugli interi, per esempio
a(b + c) = ab + ac, continuano valere.
Il campo finito degli interi modulo p è indicato con notazioni diverse. Noi utilizziamo
la notazione Zp per riferirci al campo finito modulo p. In altri testi potreste incontrare
GF(p) o anche Z/pZ.
Dobbiamo anche introdurre il concetto di gruppo in matematica; un gruppo è un insieme
di numeri con una sola operazione, come la somma o la moltiplicazione (ci sono altri
requisiti, ma sono tutti soddisfatti dai gruppi di cui parleremo in questo libro). I numeri
in Zp formano un gruppo con l’addizione. Si possono sommare due numeri e ottenere
un terzo numero del gruppo. Se volete utilizzare la moltiplicazione in un gruppo, non
potete utilizzare lo 0 (questo ha a che fare con il fatto che la moltiplicazione per 0 non
è molto interessante, e che non è possibile dividere per 0). I numeri 1, ... , p − 1 insieme
alla moltiplicazione modulo p formano un gruppo chiamato gruppo moltiplicativo modulo p
che si indica in vari modi; noi utilizzeremo la notazione Z*p. Un campo finito è costituito
da due gruppi: il gruppo di addizione e il gruppo di moltiplicazione. Nel caso di Zp il
campo finito è costituito del gruppo di addizione, definito dall’addizione modulo p, e
dal gruppo moltiplicazione Z*p.
Un gruppo può contenere un sottogruppo, costituito da alcuni degli elementi del gruppo
completo. Se si applica l’operazione del gruppo a due elementi del sottogruppo, si ottiene
di nuovo un elemento del sottogruppo. Sembra complicato, quindi riportiamo un esempio.
I numeri modulo 8 con l’addizione (modulo 8) formano un gruppo. I numeri {0, 2, 4,
6} formano un sottogruppo. Si possono sommare due di questi numeri in modulo 8 e
ottenere un altro elemento del sottogruppo. Lo stesso accade con i gruppi moltiplicativi:
il sottogruppo moltiplicativo modulo 7 è costituito dai numeri 1, ... , 6, con l’operazione di moltiplicazione in modulo 7. L’insieme {1, 6} forma un sottogruppo, così come
l’insieme {1, 2, 4}. Potete verificare che moltiplicando due numeri qualsiasi dello stesso
sottogruppo modulo 7 ottenete un altro elemento dello stesso sottogruppo.
I sottogruppi si utilizzano per velocizzare alcune operazioni di crittografia. Possono anche
essere utilizzati per attaccare sistemi, e per questo motivo è importante conoscerli.
Finora abbiamo parlato solo di addizione, sottrazione e moltiplicazione modulo un
numero primo. Per definire completamente un gruppo moltiplicativo serve anche
l’operazione inversa della moltiplicazione: la divisione. Osserviamo che si può definire
la divisione sui numeri modulo p come segue: a/b (mod p) è un numero c tale che c · b
= a (mod p). Non si può dividere per zero, ma osserviamo che la divisione a/b (mod p) è
sempre definita se b ≠ 0.
Quindi come si calcola il quoziente di due numeri in modulo p? Questa operazione
è più complicata e saranno necessarie alcune pagine per spiegarla. Dobbiamo tornare
indietro di oltre 2000 anni, fino all’epoca di Euclide e al suo algoritmo per il massimo
comun divisore.

156   Capitolo 10

10.3.4 L’algoritmo per il massimo comun divisore
Un altro ricordo del corso di matematica della scuola superiore: il massimo comun divisore
(MCD) di due numeri a e b è il massimo k tale che k | a e k | b. In altre parole, mcd(a,
b) è il massimo numero che divide sia a che b.
Euclide trovò un algoritmo per calcolare il massimo comun divisore di due numeri
che è utilizzato ancora oggi, migliaia di anni dopo. Per una discussione dettagliata di
tale algoritmo cfr. Knuth [75]. Nel seguito indichiamo la funzione con GCD (Greatest
Common Divisor) per mantenere la corrispondenza degli algoritmi con il testo originale in inglese.
function GCD
input: a Intero positivo.

b Intero positivo.
output: k Il massimo comun divisore di a e b.

assert a ≥ 0 ^ b ≥ 0

while a ≠ 0 do

(a, b) ← (b mod a, a)

od

return b
Perché questo algoritmo funziona? La prima osservazione è che l’assegnamento non
cambia l’insieme dei divisori comuni di a e b. Dopo tutto, (b mod a) p semplicemente
b − sa per qualche intero s. Qualsiasi numero k che divide sia a che b divide anche a e
(b mod a), e vale anche l’inverso. Quando a = 0, b è un divisore comune di a e b, ed è
ovviamente il massimo divisore comune. Potete verificare da soli che il ciclo deve terminare perché a e b diventano sempre più piccoli finché arrivano a zero.
Come esempio, calcoliamo il massimo comun divisore di 21 e 30. Iniziamo con (a, b)
= (21, 30). Nella prima iterazione calcoliamo (30 mod 21) = 9, perciò otteniamo (a, b)
= (9, 21). Nella successiva iterazione calcoliamo (21 mod 9) = 3, perciò otteniamo (a,
b) = (3, 9). Nell’ultima iterazione calcoliamo (9 mod 3) = 0 e otteniamo (a, b) = (0, 3).
L’algoritmo restituirà 3, che in effetti è il massimo comun divisore di 21 e 30.
Il massimo comun divisore ha un cugino, il minimo comune multiplo (MCM). Il minimo
comune multiplo di a e b è il minimo numero che è multiplo di a e di b. Per esempio,
mcm(6, 8) = 24. MCD e MCM sono strettamente legati dall’equazione:

mcm(a, b ) =

ab
mcd(a, b )

che non dimostreremo qui, ma prenderemo per valida.

10.3.5 L’algoritmo di Euclide esteso
L’algoritmo di Euclide non ci aiuta ancora a calcolare la divisione modulo p. Per questo
scopo ci serve la sua versione estesa. Il concetto è che mentre calcoliamo mcd(a, b) possiamo anche trovare due interi u e v tali che mcd(a, b) = ua + vb. Questo ci consentirà
di calcolare a/b (mod p).

I numeri primi   157

function extendedGCD
input: a Argomento intero positivo.

b Argomento intero positivo.
output: k Massimo comun divisore di a e b.

(u, v) Interi tali che ua + vb = k.

assert a ≥ 0 ^ b ≥ 0

(c, d) ← (a, b)

(uc, vc, ud, vd) ← (1, 0, 0, 1)

while c ≠ 0 do

Invariante: uca + vcb = c ^ uda + vdb = d

q ← d/c

(c, d) ← (d − qc, c)

(uc, vc, ud, vd) ← (ud − quc, vd − qvc, uc, vc)

od

return d, (ud, vd)
Questo algoritmo è molto simile a quello per il massimo comun divisore. Introduciamo
nuove variabili c e d invece di utilizzare a e b perché nell’invariante dovremo fare riferimento ad a e b originali. Se osservate soltanto c e d, questo è esattamente l’algoritmo
per il massimo comun divisore (abbiamo leggermente riscritto la formula d mod c,
ma fornisce lo stesso risultato). Abbiamo aggiunto quattro variabili che mantengono
il dato invariante; per ogni valore di c o d generato, teniamo traccia di come esprimere
tale valore come combinazione lineare di a e b. Per l’inizializzazione è facile, dato che
c è inizializzato ad a e d a b. Quando modifichiamo c e d nel ciclo, non è difficilissimo
aggiornare le variabili u e v.
Perché occuparci dell’algoritmo di Euclide esteso? Supponiamo di voler calcolare 1/b
mod p dove 1 ≤ b < p. Utilizziamo tale algoritmo per calcolare extendedGCD(b, p).
Sappiamo che il massimo comun divisore di b e p è 1, perché p è primo e quindi non
ha altri divisori adatti. Ma la funzione extendedGCD fornisce anche due numeri u e v
tali che ub + vp = mcd(b, p) = 1. In altre parole, ub = 1 − vp o ub = 1 (mod p). Questo
equivale a dire che u = 1/b (mod p), l’inverso di b modulo p. Ora si può calcolare la
divisione a/b moltiplicando a per u, ottenendo così a/b = au (mod p), e quest’ultima
formula sappiamo come calcolarla.
L’algoritmo di Euclide esteso ci consente di calcolare l’inverso dell’operazione di modulo
con un numero primo, che a sua volta ci consente di calcolare una divisione modulo p.
Con le operazioni di addizione, sottrazione, moltiplicazione e divisione modulo p siamo in
grado di calcolare tutte e quattro le operazioni elementari nel campo finito modulo p.
Notate che u potrebbe essere negativo, perciò è una buona idea ridurre u modulo p
prima di utilizzarlo come inverso di b.
Se esaminate con attenzione l’algoritmo extendedGCD vedrete che, se volete ottenere in
output soltanto u, potete rinunciare alle variabili vc e vd, che non influiscono sul calcolo di
u. Così si riduce leggermente il lavoro necessario per calcolare una divisione modulo p.

158   Capitolo 10

10.3.6 Calcolo modulo 2
Un interessante caso particolare è costituito dal calcolo modulo 2. Il numero 2 è primo,
perciò dovremmo essere in grado di utilizzarlo nel calcolo modulare. Chi ha già esperienza
di programmazione dovrebbe capire a che cosa vogliamo arrivare. Nella Figura 10.1 sono
riportate le tabelle di addizione e moltiplicazione modulo 2. L’addizione modulo 2 è
esattamente la funzione di or esclusivo (xor) utilizzata nei linguaggi di programmazione.
La moltiplicazione è semplicemente un’operazione and. Nel campo modulo 2 è possibile
soltanto un’inversione, (1/1 = 1), perciò la divisione coincide con la moltiplicazione.
Non desterà sorpresa il fatto che il campo Z2 è un importante strumento per analizzare
determinati algoritmi utilizzati dai computer.
+

0

1

0

1

0

0

1

0

0

0

1

1

0

1

0

1

Figura 10.1 Addizione e moltiplicazione modulo 2.

10.4 Numeri primi grandi
Diverse primitive crittografiche utilizzano numeri primi molto grandi, anche di molte
centinaia di cifre. Non abbiate timore, non dovrete effettuare calcoli manuali con questi
numeri. Per questo c’è il computer.
Per eseguire dei calcoli con numeri così grandi, serve una libreria in precisione multipla.
Non si possono utilizzare numeri a virgola mobile, perché non hanno centinaia di cifre
di precisione. Non si possono utilizzare i normali interi, perché nella maggior parte dei
linguaggi di programmazione gli interi possono avere al più una dozzina di cifre, più o
meno. Alcuni linguaggi forniscono il supporto nativo per interi di precisione arbitraria.
Scrivere routine per eseguire calcoli con interi grandi è un’attività affascinante. Potete
trovare una buona panoramica in Knuth [75, Paragrafo 4.3].Tuttavia, l’implementazione
di una libreria a precisione multipla richiede tantissimo lavoro. Non solo occorre fornire i
risultati corretti, ma anche ottimizzare le routine in modo che i calcoli siano eseguiti con
la massima velocità possibile. Ci sono parecchie situazioni speciali che vanno affrontate
con molta cura.Vi consigliamo di risparmiare il vostro tempo per attività più importanti
e affidarvi a una delle molte librerie gratuite disponibili su Internet, oppure di utilizzare
un linguaggio come Python che ha il supporto interno per gli interi grandi.
Per la crittografia a chiave pubblica, i primi che vogliamo generare hanno lunghezze
di 2000 – 4000 bit. Il metodo di base per generare un numero primo così grande è
sorprendentemente semplice: si prende un numero casuale e si verifica se è primo.
Esistono ottimi algoritmi per determinare se un numero sia primo o no, e i numeri
primi sono tanti. Nell’intorno di un numero n, circa uno su ln n numeri è primo (il
logaritmo naturale di n, ln n, è una delle funzioni standard disponibili su qualsiasi calcolatrice scientifica. Per darvi un’idea della lentezza con cui il logaritmo cresce al crescere
della dimensione dell’input, il logaritmo naturale di 2k è di poco minore di 0,7 · k). Un
numero lungo 2000 è compreso tra 21999 e 22000. In tale intervallo, circa uno ogni 1386

I numeri primi   159

numeri è primo. E questo comprende molti numeri che sono evidentemente composti,
come i numeri pari.
Riportiamo di seguito come si può procedere per generare un numero primo grande.
function generateLargePrime
input: l Estremo inferiore dell’intervallo in cui deve essere compreso il numero
primo.

u Estremo superiore dell’intervallo in cui deve essere compreso il numero
primo.
output: p Un numero primo casuale compreso nell’intervallo l, ... , u

Imposta un intervallo di sensibilità.

assert 2 < l ≤ u

Calcola il massimo numero di tentativi.

r ← 100(log2 u + 1)

repeat

r←r−1

assert r > 0

Sceglie n a caso nell’intervallo corretto.

n ∈ℜ l, ... , u

Continua a tentare fino a trovare un numero primo.

until isPrime(n)

return n
Utilizziamo l’operatore ∈ℜ per indicare una selezione casuale da un insieme. Naturalmente ciò richiede dati di output dal PRNG.
L’algoritmo è relativamente semplice. Per prima cosa impostiamo un intervallo di sensibilità. I casi l ≤ 2 e l ≥ u sono inutili e possono comportare dei problemi. Notate la
condizione di limite: il caso l = 2 non è consentito (l’algoritmo di Rabin-Miller utilizzato
nel seguito non funziona bene quando riceve come argomento 2. Sappiamo comunque
che 2 è primo, perciò non occorre generarlo qui). Poi calcoliamo il numero di tentativi
che effettueremo per trovare un numero primo. Esistono intervalli che non contengono
alcun numero primo; per esempio, l’intervallo 90, ... , 96 è uno di questi. Un programma
ben fatto non dovrebbe mai bloccarsi, indipendentemente dai suoi dati di input, perciò
limitiamo il numero di tentativi e generiamo un errore quando tale numero viene superato. Quante volte dovremmo provare? Come si è affermato precedentemente, nell’intorno di u circa uno ogni 0,7 log2 u numeri è primo (la funzione log2 è il logaritmo in
base 2. La definizione più semplice è log2 (x) := ln x/ ln 2). Il numero log2 u è difficile
da calcolare, ma log2 u +1 è molto più facile, infatti è il numero di bit necessari per
rappresentare u come numero binario. Perciò, se u è un intero lungo 2017 bit, allora
log2 u + 1 = 2017. Il fattore 100 garantisce che sia estremamente improbabile non
trovare un numero primo. Per intervalli sufficientemente grandi, la probabilità di fallire
è minore di 2−128, perciò possiamo ignorare il rischio. Nello stesso tempo, questo limite
non assicura che la funzione generateLargePrime terminerà. In un’implementazione
corretta, la generazione di un errore sarebbe accompagnata da una spiegazione di ciò
che è accaduto; nel nostro caso siamo stati più “grezzi”.

160   Capitolo 10

Il ciclo principale è semplice. Dopo il controllo che limita il numero di tentativi, scegliamo un numero casuale e verifichiamo se è primo utilizzando la funzione isPrime,
che definiremo tra poco.
Assicuratevi che il numero n scelto sia uniformemente casuale nell’intervallo l, ... , u e
che tale intervallo non sia troppo piccolo, se volete che il vostro numero primo rimanga
segreto. Se l’attaccante conosce l’intervallo utilizzato, e questo contiene meno di 2128
numeri primi, teoricamente sarebbe possibile provarli tutti.
Se volete, potete assicurarvi che il numero casuale generato sia dispari impostando il bit
meno significativo subito dopo aver generato un candidato n. Dato che 2 non è compreso
nell’intervallo, questo non ha conseguenze sulla distribuzione di probabilità dei numeri
primi che state scegliendo, e dimezzerà il numero di tentativi da effettuare.Tuttavia, questo
è possibile soltanto se u è dispari, altrimenti, l’impostazione del bit meno significativo
potrebbe portare n al di fuori dell’intervallo consentito. Inoltre, questo genererà piccole
deviazioni da l, se l è dispari.
La funzione isPrime è un filtro a due fasi: la prima fase è un semplice test in cui proviamo
a dividere n per tutti i numeri primi piccoli. In questo modo si esclude rapidamente la
grande maggioranza di numeri composti e divisibili per un numero primo piccolo. Se
non troviamo divisori, utilizziamo un test pesante chiamato test di Rabin-Miller.
function isPrime
input: n Intero ≥ 3.
output: b Booleano che indica se n è primo.

assert n ≥ 3

for p ∈ {tutti i primi ≤ 1000} do

if p è un divisore di n then
return p = n

fi

od

return Rabin-Miller(n)
Se siete pigri e non volete generare i numeri primi piccoli, potete ricorrere a un trucco.
Anziché provare tutti i primi, potete provare 2 e tutti i numeri dispari 3, 5, 7, ... , 999
nell’ordine. Questa sequenza contiene tutti i numeri primi minori di 1000, ma contiene
anche molti numeri composti inutili. L’ordine è importante per garantire che un piccolo
numero composto come 9 sia individuato correttamente come tale. Il limite di 1000 è
arbitrario e può essere scelto in modo da ottimizzare le prestazioni.
Non ci rimane che spiegare il misterioso test di Rabin-Miller che svolge il lavoro più
pesante.

10.4.1 Test di primalità
È piuttosto facile verificare se un numero è primo. O almeno, è facile rispetto a operazioni come scomporre un numero in fattori e trovare i suoi divisori primo. Questi test
facili non sono perfetti, sono tutti probabilistici, esiste la possibilità che diano la risposta
sbagliata. Tuttavia, eseguendo lo stesso test più volte possiamo ridurre la probabilità di
errore portandola a un livello accettabile.

I numeri primi   161

Il test di primalità di elezione è quello di Rabin-Miller. Le basi matematiche di tale test
vanno ben oltre lo scopo di questo libro, ma lo schema è abbastanza semplice. Scopo del
test è determinare se un intero dispari n è primo. Scegliamo un valore casuale a minore
di n, chiamato base, e verifichiamo una certa proprietà di a modulo n che vale sempre
quando n è primo. Tuttavia, si può dimostrare che, quando n non è primo, questa proprietà vale per al più il 25% di tutti i possibili valori della base. Ripetendo questo test
per diversi valori casuali di a, si costruisce un risultato finale sempre più affidabile. Se n
è primo, risulterà sempre tale al test. Se non è primo, almeno il 75% dei possibili valori
di a mostrerà che è tale, e la possibilità che n superi più test può essere ridotta a piacere.
Limitiamo la probabilità di un falso risultato a 2−128 per raggiungere il nostro livello di
sicurezza richiesto.
Riportiamo di seguito l’algoritmo.
function Rabin-Miller
input: n Un numero dispari ≥ 3.
output: b Booleano che indica se n è primo o no.

assert n ≥ 3 ^ n mod 2 = 1

Prima calcoliamo (s, t) tali che s è dispari e 2ts = n − 1.

(s, t) ← (n − 1, 0)

while s mod 2 = 0 do

(s, t) ← (s/2, t + 1)

od

Registriamo in k la probabilità di un falso risultato.Tale probabilità è al più 2−k. Eseguiamo
un ciclo fino a ridurre la probabilità di un falso risultato a un valore sufficientemente piccolo.

k←0

while k < 128 do

Sceglie un numero casuale a tale che 2 ≤ a ≤ n − 1.

a ∈ℜ 2, ... , n − 1

L’operazione pesante: un elevamento a potenza modulare.

v ← as mod n

Quando v = 1, il numero n supera il test per la base a.

if v ≠ 1 then
t
La sequenza v, v2, ... , v2 deve terminare sul valore 1 e l’ultimo valore diverso da
1 deve essere n − 1 se n è primo.
i ← 0
while v = n − 1 do
if i = t − 1 then
return false
else
(v, i) ← (v2 mod n, i + 1)
fi

162   Capitolo 10

od

fi

Quando arriviamo a questo punto, n ha superato il test di primalità per la base a.
Abbiamo quindi ridotto la probabilità di un falso risultato di un fattore di 22,
perciò possiamo sommare 2 a k.

k←k+2

od

return true
Questo algoritmo funziona soltanto per un n dispari maggiore o uguale a 3, perciò per
prima cosa controlliamo questa condizione. La funzione isPrime dovrebbe richiamare
questa funzione soltanto con un argomento corretto, ma ogni funzione è responsabile di
verificare input e output. Non si sa mai come verrà modificato il software in futuro.
L’idea alla base di questo test è nota come piccolo teorema di Fermat (il matematico
Fermat ha dato il nome a diversi teoremi. L’ultimo teorema di Fermat è quello più famoso, riguarda l’equazione an + bn = cn e la sua dimostrazione è troppo lunga per essere
riportata qui). Per qualsiasi numero primo n e per tutti gli 1 ≤ a < n, vale la relazione
an−1 mod n = 1. Per capire bene i fondamenti di tale teorema servirebbero nozioni di
matematica superiori a quelle che possiamo riportare qui. Un semplice test (chiamato test
di primalità di Fermat) verifica questa relazione per un numero di valori a scelti a caso.
Sfortunatamente esistono dei numeri molto fastidiosi, chiamati numeri di Carmichael,
che sono composti ma superano il test di Fermat per quasi tutte le basi a.
Il test di Rabin-Miller è una variante del test di Fermat. Innanzitutto scriviamo n − 1 come
2ts, dove s p un numero dispari. Se volete calcolare an−1 potete calcolare prima as e poi
t
elevare il risultato al quadrato t volte per ottenere as·2 = an−1. Se as = 1 (mod n), ripetute
operazioni di elevamento al quadrato non cambieranno il risultato, perciò abbiamo an−1 =
t
2
2
1 (mod n). Se as ≠ 1 (mod n), allora esaminiamo i numeri as, as·2, as·2 , a s·2 , ... , a s·2 (tutti
modulo n, naturalmente). Se n è primo, allora sappiamo che l’ultimo numero deve essere 1.
Se n è primo, allora gli unici numeri che soddisfano x2 = 1 (mod n) sono 1 e n − 1, infatti
è facile verificare che (n – 1)2 = 1 (mod n). Perciò, se n è primo, allora uno dei numeri
della sequenza deve essere n − 1, altrimenti non avremmo mai l’ultimo numero uguale
a 1. E così si concludono i controlli del test di Rabin-Miller. Se una qualsiasi scelta di a
dimostra che n è composto, terminiamo subito. Se n continua a risultare primo, ripetiamo
il test per diversi valori di a finché la probabilità che abbiamo generato un falso risultato
affermando che un numero composto è primo risulta minore di 2−128.
Se si applica questo test a un numero casuale, la probabilità di fallimento è molto minore
del limite che ci siamo posti. Per quasi tutti i numeri composti n, quasi tutti i valori della
base mostreranno che n è composto.Troverete molte librerie che si basano su questo ed
eseguono il testo soltanto per 5 o 10 basi. L’idea è buona, anche se dovremmo verificare quanti tentativi servano per raggiungere un livello di errore di 2−128 o minore, ma
vale solo se si applica il test di primalità a numeri scelti a caso. Più avanti incontreremo
situazioni in cui il test di primalità viene applicato a numeri ricevuti da altri; questi
numeri potrebbero non essere scelti a caso, perciò la funzione isPrime deve raggiungere
un limite di errore di 2−128 da sola.
Eseguire tutti i 64 test di Rabin-Miller è necessario quando il numero da verificare è
stato inviato da altri, mentre è eccessivo quando tentiamo di generare un numero primo a

I numeri primi   163

caso. Quando si genera un numero primo, tuttavia, si occupa la maggior parte del tempo
a scartare numeri composti (quasi tutti i numeri composti sono scartati dal primo test di
Rabin-Miller eseguito. Poiché potrebbe essere necessario provare centinaia di numeri
prima di trovarne uno primo, l’esecuzione di 64 test sul numero primo finale non richiede
un tempo significativamente superiore a quello necessario per eseguirne 10.
In una versione precedente di questo capitolo, la routine di Rabin-Miller aveva un secondo argomento che si poteva utilizzare per selezionare la massima probabilità di errore. Si
trattava però di un’opzione inutile, perciò l’abbiamo rimossa. La scelta di eseguire sempre
un buon test fino al limite di 2−128 è più semplice e comporta meno problemi.
Esiste ancora una probabilità di 2−128 che la nostra funzione isPrime fornisca un risultato
errato. Per darvi un’idea di quanto piccola sia tale probabilità, considerate che la probabilità che veniate uccisi da un meteorite mentre state leggendo questa frase è molto
superiore. Siete ancora vivi? Bene, allora non preoccupatevi.

10.4.2 Calcolo di potenze
Il test Rabin-Miller passa gran parte del tempo nel calcolo di as mod n. Non si può
calcolare prima as e poi eseguirne il modulo n. Nessun computer al mondo ha memoria
sufficiente per memorizzare as, figurarsi la potenza necessaria per calcolarlo. a e s possono
essere lunghi migliaia di bit. Tuttavia, a noi serve soltanto as mod n; possiamo applicare
l’operazione mod n a tutti i risultati intermedi, evitando così che i numeri diventino
troppo grandi.
Esistono diversi modi per calcolare as mod n, nel seguito ne è descritto uno semplice.
Per calcolare as mod n, utilizzate le regole seguenti:
• Se s = 0 il risultato è 1.
• Se s > 0 e s è pari, calcolate prima y := as/2 mod n utilizzando queste stesse regole.
Il risultato è dato da as mod n = y2 mod n.
• Se s > 0 e s è dispari, calcolate prima y := a(s−1)/2 mod n utilizzando queste stesse
regole. Il risultato è dato da as mod n = a · y2 mod n.
Questa è una formulazione ricorsiva del cosiddetto algoritmo binario. Se esaminate le
operazioni eseguite, noterete che costruisce l’esponente desiderato un bit dopo l’altro,
dalla parte più significativa fino a quella meno significativa. È anche possibile convertire
questo algoritmo ricorsivo in un ciclo.
Quante moltiplicazioni sono necessarie per calcolare as mod n? Sia k il numero di bit di s;
ovvero, 2k−1 ≤ s < 2k. Allora questo algoritmo richiede al più 2k moltiplicazioni modulo
n. Non è male. Se verifichiamo la primalità di un numero di 2000 bit, allora anche s sarà
lungo circa 2000 bit e ci serviranno soltanto 4000 moltiplicazioni. È sempre parecchio
lavoro, ma certamente alla portata della maggior parte dei computer desktop.
Molti sistemi di crittografia a chiave pubblica utilizzano operazioni di elevamento a potenza modulare. Qualsiasi libreria a precisione multipla ben fatta dispone di una routine
ottimizzata per calcolare l’elevamento a potenza modulare. Per questo compito è particolarmente indicato un particolare tipo di moltiplicazione, chiamato moltiplicazione di
Montgomery. Esistono anche dei metodi di calcolare as utilizzando un minor numero di
moltiplicazioni [18]. Ognuna di queste tecniche può consentire di risparmiare dal 10% al
30% del tempo necessario per calcolare un’operazione di elevamento a potenza modulare,
perciò utilizzandole in combinazione si possono ottenere risultati importanti.

164   Capitolo 10

Le implementazioni grezze dell’elevamento a potenza modulare sono spesso vulnerabili
ad attacchi basati sulla temporizzazione. Si rimanda al Paragrafo 15.3 per i dettagli e i
possibili rimedi.

10.5 Esercizi
Esercizio 10.1 Implementate SmallPrimeList. Qual è la perfomance nel caso peggiore? Generate un grafico dei tempi ottenuti dalla vostra implementazione con n = 2,
4, 8, 16, ... , 220.
Esercizio 10.2 Calcolate 13635 + 16060 + 8190 + 21363 (mod 29101) in due modi
e verificate l’equivalenza di due metodi: ridurre modulo 29101 dopo ogni somma, calcolare prima l’intera somma e poi ridurre modulo 29101.
Esercizio 10.3 Calcolate il risultato di 12358 · 1854 · 14303 (mod 29101) in due modi
e verificate l’equivalenza di due metodi: ridurre modulo 29101 dopo ogni moltiplicazione, calcolare prima l’intero prodotto e poi ridurre modulo 29101.
Esercizio 10.4 {1, 3, 4} è un sottogruppo del gruppo moltiplicativo degi interi
modulo 7?
Esercizio 10.5 Utilizzate l’algoritmo del massimo comun divisore per calcolare l’MCD
di 91261 e 117035.
Esercizio 10.6 Utilizzate l’algoritmo ExtendedGCD per calcolare l’inverso di 74
modulo 167 (167 è un numero primo).
Esercizio 10.7 Implementate GenerateLargePrime utilizzando un linguaggio o una
libreria che supporti interi grandi. Generate un numero primo compreso nell’intervallo
l = 2255 e u = 2256 − 1.
Esercizio 10.8 Fornite lo pseudocodice per la routine di elevamento a potenza descritta
nel Paragrafo 10.4.2. Non deve essere ricorsivo ma utilizzare un ciclo.
Esercizio 10.9 Calcolate 2735 (mod 569) utilizzando la routine di elevamento a potenza
descritta nel Paragrafo 10.4.2. Quante moltiplicazioni avete dovuto effettuare?

Capitolo 11

Diffie-Hellman

Nella discussione della crittografia a chiave pubblica
seguiremo il percorso storico. Le origini della crittografia a chiave pubblica risalgono al momento in
cui Whitfield Diffie e Martin Hellman pubblicarono
il loro articolo “New Directions in Cryptography”
nel 1976 [33].
Finora in questo libro abbiamo parlato soltanto di
cifratura e autenticazione con chiavi segrete condivise, ma dove possiamo procurarci queste chiavi?
Se avete 10 amici con cui volete comunicare, potete
incontrarli tutti e scambiare una chiave segreta con
ciascuno di essi, da utilizzare in futuro. Ma come
tutte le chiavi, anche queste devono essere aggiornate regolarmente, perciò a un certo punto dovrete
incontrare nuovamente tutti i vostri amici. Per un
gruppo di 10 amici servono in totale 45 chiavi, ma
quando il gruppo cresce, il numero di chiavi cresce in
modo quadratico. Per 100 persone che comunicano
tra loro servono 4950. In generale, per un gruppo di
N persone servirebbero N(N − 1)/2 chiavi. Il numero
diventa rapidamente impossibile da gestire.
Diffie e Hellman si chiesero se fosse possibile gestire
tutto ciò in modo più efficiente. Supponete di avere
un algoritmo di cifratura in cui le chiavi di cifratura e
decifratura sono diverse. Potreste rendere pubblica la
vostra chiave di cifratura e mantenere segreta quella di
cifratura; in questo caso, chiunque potrebbe inviarvi
un messaggio cifrato, ma solo voi sareste in grado di
decifrarlo. In questo modo si risolverebbe il problema
di dover distribuire tante chiavi diverse.
Diffie e Hellman furono in grado di fornire soltanto
una risposta parziale alla loro domanda, e la loro so-

Sommario
11.1 I gruppi
11.2 Il protocollo DH
originale
11.3 L’attacco man-in-themiddle
11.4 Trappole
11.5 Numeri primi sicuri
11.6 Utilizzo di un
sottogruppo più
piccolo
11.7 La dimensione di p
11.8 Regole pratiche
11.9 Che cosa può andare
male?
11.10 Esercizi

166   Capitolo 11

luzione parziale è nota come protocollo di scambio di chiavi di Diffie-Hellman, o in
breve protocollo DH [33].
Il protocollo DH è un’idea davvero eccellente. Due persone che comunicano su una
linea non sicura possono accordarsi su una chiave segreta in modo da poter utilizzare la
stessa chiave, senza divulgarla ad altri eventualmente in ascolto sulla loro linea.

11.1 I gruppi
Se avete letto il capitolo precedente non sarete sorpresi di dover affrontare i numeri
primi. Nella parte restante di questo capitolo, p è un numero intero grande, che potete
considerare lungo da 2000 a 4000 bit. La maggior parte dei calcoli svolti in questo capitolo sarà modulo p, anche se non lo specificheremo in modo esplicito. Il protocollo DH
utilizza Z*p, il gruppo moltiplicativo modulo p trattato nel Paragrafo 10.3.3.
Scegliete un g qualsiasi nel gruppo e considerate i numeri 1, g, g2 , g3 , ... , tutti modulo p,
naturalmente. Questa è una sequenza infinita di numeri, ma Z*p contiene solo un insieme
finito di numeri (ricordate che Z*p è l’insieme dei numeri 1, ..., p – 1 con l’operazione
di moltiplicazione modulo p). A un certo punto, quindi, i numeri dovranno cominciare
a ripetersi. Supponiamo che ciò accada in gi = gj con i < j. Poiché sappiamo eseguire
la divisione modulo p, dividiamo ciascun membro per gi e otteniamo 1 = gj–i. In altre
parole, esiste un numero q := j − i tale che gq = 1 (mod p). Chiamiamo ordine di g il più
piccolo valore positivo q per cui gq = 1 (mod p). Sfortunatamente la terminologia di
questo campo è complessa, ma preferiamo utilizzare termini standard anziché inventarne
di nuovi; in questo modo sarete favoriti quando leggerete altri libri sul tema.
Se continuiamo a moltiplicare i g, possiamo ottenere i numeri 1, g, g2, ... , gq−1, dopodiché
la sequenza si ripete, poiché gq = 1. Diciamo che g è un generatore e genera l’insieme
1, g, g2, ... , gq−1. Il numero di elementi che possono essere scritti come potenza di g è
esattamente q, l’ordine di g.
Una proprietà della moltiplicazione modulo p è che esiste almeno un g che genera
l’intero gruppo. Esiste cioè almeno un valore g per cui q = p − 1. Perciò, anziché di
pensare a Z*p come l’insieme dei numeri 1, ... , p − 1, possiamo anche pensare ai numeri
1, g, g2, ... , gp−2. Un g che genera l’intero gruppo si chiama elemento primitivo del gruppo
in questione.
Altri valori di g possono generare insiemi più piccoli. Notate che, se moltiplichiamo
due numeri dell’insieme generato da g, otteniamo un’altra potenza di g, quindi un altro
elemento dell’insieme. Svolgendo tutti i passaggi matematici si determina che l’insieme
generato da g è un altro gruppo. Quindi è possibile effettuare moltiplicazioni e divisioni
in questo gruppo esattamente come nell’ampio gruppo modulo p. Questi gruppi più
piccoli sono chiamati sottogruppi (cfr. Paragrafo 10.3.3) e possono essere importanti in
vari tipi di attacchi.
Rimane da spiegare un ultimo punto. Per qualsiasi elemento g, l’ordine di g è un divisore di p − 1. Non è difficile capire perché. Sia g un elemento primitivo, e sia h un
altro elemento qualsiasi. Poiché g genera l’intero gruppo, esiste un x tale che h = gx. Ora
considerate gli elementi generati da h; sono 1, h, h2, h3, ... che sono uguali a 1, gx, g2x, g3x,
... (tutti i calcoli sono sempre modulo p, naturalmente). L’ordine di h è il più piccolo
q per cui hq = 1, che come dire che è il più piccolo q tale che gxq = 1. Per qualsiasi t,
scrivere gt = 1 equivale a scrivere t = 0 (mod p − 1). Perciò q è il più piccolo q tale che

Diffie-Hellman   167

xq = 0 (mod p − 1). Questo si verifica quando q = (p − 1)/mcd(x, p − 1), perciò q è
ovviamente un fattore di p − 1.
Presentiamo un semplice esempio. Sia p = 7. Se poniamo g = 3, allora g è un elemento
primitivo perché 1, g, g2, ... , g5 = 1, 3, 2, 6, 4, 5 (anche qui tutti i calcoli sono modulo p).
L’elemento h = 2 genera il sottogruppo 1, h, h2 = 1, 2, 4 perché h3 = 23 mod 7 = 1.
L’elemento h = 6 genera il sottogruppo 1, 6. Questi sottogruppi hanno dimensione 3 e
2, rispettivamente, e questi numeri sono entrambi divisori di p − 1.
Ciò spiega anche parte del test di Fermat di cui abbiamo parlato nel Paragrafo 10.4.1.
Il test di Fermat si basa sul fatto che per ogni a abbiamo ap−1 = 1. La verifica è semplice:
sia g un generatore di Z*p e sia x tale che gx = a. Poiché g è un generatore dell’intero
gruppo, esiste sempre un tale x. Ma ora ap−1 = gx(p−1) = (gp−1)x = 1x = 1.

11.2 Il protocollo DH originale
Per il protocollo DH originale scegliamo innanzitutto un grande numero primo p e
un elemento primitivo g che genera l’intero gruppo Z*p. p e g sono entrambi costanti
pubbliche in questo protocollo, e supponiamo che tutte le parti interessate, inclusi gli
attaccanti, li conoscano. Il protocollo è mostrato nella Figura 11.1, che presenta uno dei
modi consueti in cui si scrivono protocolli di crittografia. Sono coinvolte sue parti: Alice
e Bob. Il tempo avanza andando dall’alto verso il basso. Prima Alice sceglie un x casuale
in Z*p, che equivale a scegliere un numero casuale in 1, ... , p − 1. Alice calcola gx mod
p e invia il risultato a Bob, che a sua volta sceglie un y casuale in Z*p, calcola gy mod p
e invia il risultato ad Alice. Il risultato finale k è definito come gxy. Alice può calcolarlo
elevando gy che ha ottenuto da Bob all’esponente x che conosce (matematica da scuola
superiore: (gy)x = gxy).Analogamente, Bob può calcolare k come (gx)y. Entrambi ottengono
lo stesso valore k, che possono utilizzare come chiave segreta.
E che cosa succede in caso di attacco? L’attaccante riesce a vedere gx e gy, ma non x o
y. Il problema di calcolare gxy dati gx e gy è noto come problema di Diffie-Hellman, o
in breve DH. Purché p e g siano scelti correttamente, non si conosce alcun algoritmo
efficiente per calcolare quanto chiesto. Il metodo migliore consiste nel calcolare prima
x da gx, dopodiché l’attaccante può calcolare k come (gy)x esattamente come ha fatto
Alice. Nei numeri reali il calcolo di x a partire da gx è dato dalla funzione logaritmo,
che si trova disponibile su qualsiasi calcolatrice scientifica. Nel campo finito Z*p si parla
di logaritmo discreto, e in generale il problema di calcolare x da gx in un gruppo finito è
noto come problema del logaritmo discreto.

Alice

x ∈ Z*p

Bob
gx
g

y

k ← (g y ) x
Figura 11.1 Il protocollo Diffie-Hellman originale.

y ∈ Z*p
k ← (g x ) y

168   Capitolo 11

Il protocollo DH originale può essere utilizzato in molti modi. Lo abbiamo scritto come
scambio di messaggi tra due parti. Un altro modo per utilizzarlo consiste nel consentire
a chiunque di scegliere un x casuale e pubblicare gx (mod p) nell’equivalente digitale
di un elenco telefonico. Se Alice vuole comunicare con Bob in modo sicuro, ottiene gy
dall’elenco telefonico e, utilizzando il suo x, calcola gxy. Bob analogamente può calcolare
gxy senza alcuna interazione con Alice. Il sistema è dunque utilizzabile in ambienti quali
la posta elettronica, in cui non vi è interazione diretta.

11.3 L’attacco man-in-the-middle
L’unico tipo di attacco per cui il protocollo DH non offre protezione è quello denominato
man-in-the-middle (la terminologia può apparire simile, ma l’attacco man-in-the-middle
è diverso da quello denominato meet-in-the-middle descritto nel Paragrafo 2.7.2).
Torniamo a esaminare il protocollo. Alice sa che sta comunicando con qualcuno, ma
non sa chi è. Eve può inserirsi nel mezzo del protocollo e fingere di essere Bob quando
parla con Alice, fingere di essere Alice quando parla con Bob. La situazione è mostrata
nella Figura 11.2. Per Alice, questo protocollo appare identico al protocollo DH originale. Alice non ha modo di determinare che sta parlando con Eve e non con Bob. Eve
può continuare a piacimento con le sue finzioni. Supponete che Alice e Bob inizino a
comunicare utilizzando la chiave segreta che credono di aver impostato. Tutto ciò che
Eve deve fare è seguire tutte le comunicazioni tra Alice e Bob. Naturalmente Eve deve
decifrare tutti i dati ottenuti da Alice e cifrati con la chiave k, e poi cifrarli nuovamente
con la chiave k per inviarli a Bob. Deve fare lo stesso anche con il traffico nella direzione
opposta, ma questo non è un gran lavoro.
Con un elenco di contatti digitale, questo attacco è più difficile. Se chi pubblica l’elenco
verifica l’identità di chiunque quando invia gx, Alice sa che sta utilizzando il gx di Bob.
Esamineremo altre soluzioni più avanti, quando parleremo di firme digitali e PKI.

Alice

x ∈ Z*p

Eve

Bob

gx
v ∈ Z*p

gv
gy

g
k ← (g w ) x

w

y ∈ Z*p

w∈ Z*p
k ← (g x ) w
k ← (g y ) v

k ← (g v ) y

Figura 11.2 Il protocollo Diffie-Hellman con Eve nel mezzo in un attacco man-in-the-middle.

Diffie-Hellman   169

Esiste almeno una situazione in cui l’attacco man-in-the-middle può essere risolto
senza ulteriori infrastrutture. Se la chiave k è utilizzata per cifrare una conversazione
telefonica (o una connessione video), Alice può parlare a Bob e riconoscerlo per la sua
voce. Sia h una funzione di hash di qualche tipo. Se Bob legge le prime cifre di h(k) ad
Alice, quest’ultima può verificare che Bob sta utilizzando la sua stessa chiave. Alice può
leggere a Bob alcune cifre seguenti di h(k), per consentire a Bob di effettuare la stessa
verifica. La strategia funziona, ma soltanto in situazioni in cui è possibile collegare la
conoscenza della chiave k alla persona con cui si sta effettivamente comunicando. Nella
maggior parte delle comunicazioni via computer, questa soluzione non è praticabile.
Inoltre, se Eve riuscisse a costruire un sintetizzatore vocale in grado di emulare la voce di
Bob, crollerebbe tutto. Infine, il principale problema di questa soluzione è che richiede
un comportamento disciplinato da parte degli utenti, cosa rischiosa perché gli utenti
ignorano regolarmente le procedure di sicurezza. In generale è molto meglio impostare
meccanismi tecnici per vanificare gli attacchi del tipo man-in-the-middle.

11.4 Trappole
L’implementazione del protocollo DH può risultare difficile. Per esempio, se Eve intercetta
le comunicazioni e sostituisce gx e gy con il numero 1, Alice e Bob otterranno entrambi
k = 1. Il risultato è un protocollo di negoziazione della chiave che appare completato
con successo, mentre Eve conosce la chiave risultante. È un bel problema, e dovremo
evitare in qualche modo questo attacco.
Un altro problema si ha se il generatore g non è un elemento primitivo di Z*p ma genera
soltanto un piccolo sottogruppo. Supponiamo che g abbia un ordine di un milione. In
questo caso, l’insieme 1, g, g2, ... , gq−1 contiene soltanto un milione di elementi. Poiché
k fa parte dell’insieme, Eve può facilmente cercare la chiave corretta. Ovviamente, uno
dei requisiti è che g deve avere un ordine elevato. Ma chi sceglie p e g? Tutti gli utenti
utilizzano gli stessi valori, perciò la maggior parte di essi li ottiene da qualcun altro. Per
garantire la sicurezza, devono verificare che p e g siano scelti in modo opportuno. Alice
e Bob devono verificare che p sia primo e che g sia un elemento primitivo modulo p.
I sottogruppi modulo p costituiscono un problema separato. L’attacco di Eve che sostituisce gx con il numero 1 si contrasta facilmente facendo in modo che Bob verifichi questa
circostanza. Ma Eve potrebbe anche sostituire gx con il numero h, dove h ha un ordine
non elevato. La chiave che Bob ricava ora proviene dal piccolo insieme generato da h,
ed Eve può provare tutti i possibili valori per trovare k (naturalmente Eve può portare
lo stesso attacco contro Alice). Alice e Bob devono entrambi verificare che i numeri che
ricevono non generino piccoli sottogruppi.
Esaminiamo i sottogruppi. Lavorando modulo un numero primo, tutti i sottogruppi
(moltiplicativi) possono essere generati da un singolo elemento. L’intero gruppo Z*p
consiste degli elementi 1, ... , p − 1, in totale p − 1. Ogni sottogruppo ha la forma 1, h,
h2, h3, ... , hq−1 per un certo h, dove q è l’ordine di h. Come si è detto in precedenza, q
deve essere un divisore di p − 1. In altre parole, la dimensione di qualsiasi sottogruppo
è un divisore di p − 1.Vale anche il viceversa: per qualsiasi divisore d di p − 1 esiste un
singolo sottogruppo di dimensione d. Se vogliamo evitare i piccoli sottogruppi, dobbiamo
evitare i piccoli divisori di p − 1.

170   Capitolo 11

Esiste un altro motivo per voler lavorare con sottogruppi grandi. Risulta che, se la scomposizione in fattori primi di p − 1 è nota, allora il calcolo del logaritmo discreto di gx
può essere suddiviso in un insieme di calcoli logaritmici discreti su sottogruppi.
Questo è un problema. Se p è un numero primo grande, allora p − 1 è sempre pari, e
quindi divisibile per 2. Allora esiste un sottogruppo con due elementi, 1 e p − 1. Ma
a parte questo sottogruppo sempre presente, possiamo evitare altri sottogruppi piccoli
stabilendo che p − 1 non abbia altri fattori piccoli.

11.5 Numeri primi sicuri
Una soluzione consiste nell’utilizzare un primo sicuro per p. Un primo sicuro è un numero
primo (abbastanza grande) p della forma 2q + 1, dove q è anch’esso primo. Il gruppo
moltiplicativo Z*p ora ha i seguenti sottogruppi:
• il sottogruppo banale costituito soltanto dal numero 1;
• il sottogruppo di dimensione 2 costituito da 1 e p − 1;
• il sottogruppo di dimensione q;
• il gruppo completo di dimensione 2q.
I primi due sono banali e da evitare. Il terzo è il gruppo che vogliamo utilizzare. Il
gruppo completo ha un altro problema. Considerate l’insieme di tutti i numeri p che si
possono scrivere come quadrati di un altro numero (modulo p, naturalmente). Risulta che
esattamente la metà dei numeri in 1, ... , p − 1 sono quadrati e l’altra metà no. Qualsiasi
generatore dell’intero gruppo non è quadrato (se fosse un quadrato non sarebbe possibile
trovare un esponente a cui elevarlo per generare un numero non quadrato, e quindi non
genererebbe l’intero gruppo).
Esiste una funzione matematica chiamata simbolo di Legendre che determina se un numero modulo p è un quadrato o meno, senza la necessità di calcolarne la radice quadrata.
Esistono algoritmi efficienti per calcolare il simbolo di Legendre. Perciò, se g non è un
quadrato e si invia gx, qualsiasi osservatore, come Eve, può determinare immediatamente
se x è pari o dispari. Se x è pari, allora gx è un quadrato. Se x è dispari, allora gx non è un
quadrato. Poiché Eve può determinare se un numero è un quadrato utilizzando la funzione simbolo di Legendre, può determinare se x è dispari o pari; non può determinare
il valore x, fatta eccezione per il bit meno significativo. La soluzione per evitare questo
problema consiste nell’utilizzare soltanto quadrati modulo p, e questo è esattamente il
sottogruppo di ordine q. Un’altra interessante proprietà è che q è primo, perché non vi
sono ulteriori sottogruppi di cui preoccuparsi.
Spieghiamo come si utilizza un primo sicuro. Scegliete (p, q) tali che p = 2q + 1 e p e
q siano entrambi primi (potete utilizzare la funzione isPrime procedendo per tentativi).
Scegliete un numero casuale α nell’intervallo 2, ... , p − 2 e impostate g = α2 (mod p).
Verificate che g ≠ 1 e g ≠ p − 1 (se g è uno di questi valori proibiti, scegliete un altro α e
provate ancora). L’insieme di parametri risultante (p, q, g) è adatto all’uso nel protocollo
di Diffie-Hellman.
Ogni volta che Alice (o Bob) riceve un valore che dovrebbe essere una potenza di g,
deve verificare che tale valore sia effettivamente contenuto nel sottogruppo generato da
g. Quando si utilizza un primo sicuro nel modo appena descritto, si può usare la funzione simbolo di Legendre per verificare l’appartenenza a un sottogruppo. C’è anche

Diffie-Hellman   171

un metodo più semplice, ma più lento. Un numero r è un quadrato se e solo se rq = 1
(mod p). Volete proibire anche il valore 1, poiché il suo utilizzo porta dei problemi. Il
testo completo quindi è il seguente: r ≠ 1 ^ r q mod p = 1.

11.6 Utilizzo di un sottogruppo più piccolo
Lo svantaggio dell’approccio con il primo sicuro è che è inefficiente. Se il numero primo
p è lungo n bit, allora q è lungo n − 1 bit e quindi tutti gli esponenti sono lunghi n − 1
bit. L’operazione di elevamento a potenza “media” richiederà circa 3n/2 moltiplicazioni
di numeri modulo p. Per numeri primi p grande, si tratta di parecchio lavoro.
La soluzione standard consiste nell’utilizzare un sottogruppo più piccolo. Iniziamo scegliendo per q un numero primo a 256 bit (in altre parole: 2255 < q < 2256). Poi troviamo
un numero primo p molto più grande tale che p = Nq + 1 per qualche valore arbitrario
N. A questo scopo, scegliamo un N a caso nell’intervallo richiesto, calcoliamo p = Nq +
1 e verifichiamo se p è primo. Poiché p deve essere dispari, è facile vedere che N deve
essere pari. Il primo p sarà lungo migliaia di bit.
Poi dobbiamo trovare un elemento di ordine q. Procediamo in modo simile al primo
caso. Scegliamo un α casuale in Z*p e impostiamo g := αN. Ora verifichiamo che g ≠ 1 e
gq = 1 (il caso g = p − 1 è coperto dal secondo test, poiché q è dispari). Se g non va bene,
scegliamo un α diverso e riproviamo. L’insieme di parametri risultante (p, q, g) è adatto
all’uso nel protocollo di Diffie-Hellman.
Quando utilizziamo questo sottogruppo più piccolo, i valori che Alice e Bob scambiano
sono tutti nel sottogruppo generato da g. Ma Eve potrebbe interferire e sostituire un
valore con un altro completamente diverso. Perciò, ogni volta che Alice o Bob riceve un
valore che dovrebbe esser contenuto nel sottogruppo generato da g, deve verificare che
lo sia davvero. Questa verifica è identica a quella descritta per il caso del primo sicuro.
Un numero r fa parte del sottogruppo corretto se r ≠ 1 ? r q mod p = 1. Naturalmente
Alice e Bob devono anche verificare che r non sia al di fuori dell’insieme dei numeri
modulo p, perciò la verifica completa diventa 1 < r < p ^ r q = 1.
Per tutti i numeri r nel sottogruppo generato da g abbiamo che r q = 1. Perciò, se avete la
necessità di elevare il numero r a un esponente e, dovete semplicemente calcolare r emodq,
con una quantità di lavoro nettamente inferiore rispetto a quella necessaria per calcolare
r e direttamente se e è molto maggiore di q.
Quanto è più efficiente il caso del sottogruppo? Il numero primo grande p è lungo
almeno 2000 bit. Nel caso del primo sicuro, il calcolo di un generico gx richiede circa
3000 moltiplicazioni. Nel caso del sottogruppo servono circa 384 moltiplicazioni, perché
x può essere ridotto modulo q e quindi è lungo soltanto 256 bit. Si ha un risparmio
quasi di un fattore otto. Quando p aumenta, il risparmio aumenta ancora di più. Ecco
perché i sottogruppi sono tanto utilizzati.

11.7 La dimensione di p
Scegliere le dimensioni giuste per i parametri di un sistema DH non è facile. Finora
abbiamo utilizzato il requisito che un attaccante deve impiegare 2128 passaggi per attaccare il sistema; era un obiettivo facile per tutte le primitive a chiave simmetrica. I

172   Capitolo 11

sistemi a chiave pubblica come DH sono molto più costosi in termini computazionali,
e il costo computazionale aumenta ancora più velocemente all’aumentare del livello di
sicurezza desiderato.
Se manteniamo il nostro requisito di costringere l’attaccante a utilizzare 2128 passaggi per
attaccare il sistema, il numero primo p dovrebbe essere lungo circa 6800 bit. Nei sistemi
reali di oggi, ciò costituirebbe un serio problema dal punto di vista delle prestazioni.
C’è una notevole differenza tra le dimensioni delle chiavi per primitive simmetriche e
per primitive a chiave pubblica come DH. Non cadete mai nell’errore di confrontare
una dimensione di chiave simmetrica (per esempio 128 o 256 bit) con quella di una
chiave pubblica che può essere di migliaia di bit. Le dimensioni delle chiavi pubbliche
sono sempre superiori a quelle delle chiavi simmetriche (questo vale per gli schemi a
chiave pubblica discussi in questo libro; altri schemi a chiave pubblica, come quelli basati
su curve ellittiche, possono avere dimensioni delle chiavi del tutto diverse).
Le operazioni dei sistemi a chiave pubblica sono molto più lente rispetto alle funzioni
di cifratura e autenticazione presentate in precedenza. Nella maggior parte dei sistemi
le operazioni con chiave simmetrica sono insignificanti dal punto di vista delle prestazioni, mentre quelle con chiave pubblica possono avere un impatto notevole. Dobbiamo
quindi esaminare molto più da vicino gli aspetti prestazionali delle operazioni con chiave
pubblica.
Le dimensioni delle chiavi simmetriche sono generalmente fissate in un sistema. Una
volta progettato un sistema per utilizzare un particolare cifrario a blocchi e una funzione
di hash, si fissa la dimensione della chiave. Ciò significa che tale dimensione è fissata per
l’intera vita del sistema. Le dimensioni delle chiavi pubbliche, invece, sono quasi sempre
variabili. In questo modo è molto più facile cambiarle. Il nostro scopo in questo libro è
progettare un sistema che sarà utilizzato per 30 anni, e i dati dovranno essere mantenuti
sicuri per 20 anni dopo la prima elaborazione. La dimensione della chiave simmetrica
deve essere scelta in modo da proteggere i dati per 50 anni a partire da ora, ma le dimensioni delle chiavi pubbliche devono proteggere i dati solo per i prossimi 20 anni.Tutte le
chiavi hanno una vita limitata. Una chiave pubblica potrebbe essere valida per un solo
anno e dover proteggere i dati per altri 20 anni. Ciò significa che la chiave pubblica deve
proteggere i dati soltanto per 21 anni, invece dei 50 anni richiesti per le chiavi simmetriche. Ogni anno si genera una nuova chiave pubblica e così è possibile scegliere chiavi
pubbliche di dimensione maggiore con i progressi delle tecnologie di calcolo.
Le migliori stime della dimensione del numero primo p si trovano in [85]. Per numero
primo di 2048 bit è lecito attendersi la protezione dei dati fino all’anno 2022, più o meno;
con 3072 bit si arriva fino al 2038 e con 4096 bit fino al 2050. La lunghezza di 6800 bit
citata in precedenza si ricava dalle formule riportate in [85]. Quella è la dimensione di
p se si vuole costringere l’attaccante a eseguire 2128 passaggi in un attacco.
Prestate molta attenzione a questi tipi di previsioni. Questi numeri hanno sempre una
base ragionevole, ma prevedere il futuro è sempre pericoloso. Potremmo essere in grado
di effettuare previsioni sensate sulle dimensioni delle chiavi per i prossimi 10 anni, ma
considerare addirittura 50 anni da oggi ha poco senso. Provate a confrontare lo stato
dell’arte dell’informatica e della crittografia ai giorni nostri con 50 anni fa. Le previsioni riportate in [85] sono decisamente le migliori stime che possediamo, ma vanno
comunque trattate con cautela.
E allora che fare? I progettisti di sistemi crittografici devono scegliere una dimensione
della chiave in grado di fornire protezione per almeno i prossimi 20 anni. Naturalmente

Diffie-Hellman   173

2048 bit è un limite inferiore. Maggiore è la lunghezza e meglio è, ma le chiavi più grandi
comportano un costo significativo. In una situazione di grande incertezza, preferiamo la
prudenza, perciò ecco il nostro consiglio: a oggi, considerate 2048 bit come il minimo
assoluto (e non dimenticate che con il passare del tempo il minimo aumenterà). Se dal
punto di vista delle prestazioni risulta possibile, utilizzate 4096 bit, o il valore più vicino
che potete sostenere. Inoltre, assicuratevi che il vostro sistema sia in grado di gestire
dimensioni fino a 8192 bit. Questo vi salverà in caso di sviluppi inattesi nelle tecniche
di attacco a sistemi a chiave pubblica. I miglioramenti compiuti nella crittoanalisi probabilmente porteranno alla possibilità di attaccare i sistemi con dimensioni delle chiavi
inferiori. Il passaggio a una dimensione della chiave molto più grande può essere attuato
mentre il sistema è in opera. Si avrà un costo in termini di prestazioni, ma l’attività di
base del sistema verrà preservata. È molto meglio di perdere tutta la sicurezza e dover
riprogettare il sistema, cosa che sarebbe necessaria se il sistema in questione non fosse in
grado di utilizzare chiavi più grandi.
Alcune applicazioni richiedono che i dati siano mantenuti segreti per molto più di 20
anni. In questo caso è necessario utilizzare le chiavi più grandi fin da ora.

11.8 Regole pratiche
Riportiamo di seguito le nostre regole pratiche per impostare un sottogruppo da utilizzare per il protocollo DH.
Scegliete come q un numero primo a 256 bit (esistono attacchi di collisione sull’esponente
in DH, perciò tutti gli esponenti devono essere lunghi 256 bit per costringere l’attaccante a utilizzare almeno 2128 passaggi). Scegliete come p un numero primo grande della
forma Nq + 1 per un determinato intero N (cfr. il Paragrafo 11.7 per una discussione
sulla dimensione di p. Calcolare l’intervallo corrispondente per N è banale). Scegliete un
numero casuale g tale che g ≠ 1 e gq = 1 (il modo più facile per farlo è quello di scegliere
un numero casuale α, impostare g = αN e verificare se g va bene, quindi provare un altro
α se g non soddisfa i criteri).
Ogni utente che riceva la descrizione di sottogruppo (p, q, g) deve verificare che:
• p e q siano entrambi primi, q sia lungo 256 bit e p sia sufficientemente grande (non
fidatevi di chiavi troppo piccole);
• q sia un divisore di (p − 1);
• g ≠ 1 e gq = 1.
Questo deve essere fatto anche se la descrizione è fornita da una fonte fidata. Sareste
sorpresi di sapere quanto spesso i sistemi falliscono in modi stravaganti, soprattutto quando
sono sotto attacco. La verifica di un insieme (p, q, g) richiede un po’ di tempo, ma nella
maggior parte dei sistemi lo stesso sottogruppo viene utilizzato per lungo tempo, perciò
questi controlli devono essere eseguiti una volta sola.
Quando un utente riceve un numero r che dovrebbe far parte del sottogruppo, deve
verificare che 1 < r < p e rq = 1. Notate che r = 1 non è consentito.
Utilizzando queste regole otteniamo la versione del protocollo Diffie-Hellman mostrata nella Figura 11.3. Entrambi gli utenti iniziano verificando i parametri di gruppo.
Ognuno di essi deve farlo soltanto una volta all’inizio, e non ogni volta che esegue un
protocollo DH (deve però farlo dopo ogni riavvio o reinizializzazione, perché i parametri
potrebbero essere cambiati).

174   Capitolo 11

Alice

Bob

noti: (p, q, g)
verifica i parametri (p, q, g)
x ∈ 1, . . . , q − 1

noti: (p, q, g)
verifica i parametri (p, q, g)
X := g x
?

Y := g y
?

?

?

?

1 < X < p , Xq = 1
y ∈ 1, . . . , q − 1

?

1 < Y < p , Yq = 1
k ← (Y) x

k ← (X) y

Figura 11.3 Diffie-Hellman in un sottogruppo.

Il resto del protocollo è praticamente identico al protocollo DH originale della Figura 11.1. Alice e Bob ora utilizzano il sottogruppo, perciò i due esponenti x e y sono
compresi nell’intervallo 1, ... , q − 1. Sia Alice che Bob controllano che il numero che
ricevono appartenga al sottogruppo corretto per evitare qualsiasi attacco ai sottogruppi
portato da Eve.
La notazione utilizzata per i controlli è un operatore relazionale (come = o <) sopra
il quale è riportato un punto di domanda. Questo significa che Alice (o Bob) devono
verificare che la relazione sia valida. Se lo è, va tutto bene. Se la relazione non è valida,
allora Alice deve assumere di essere sotto attacco. Il comportamento standard prevede di
interrompere l’esecuzione del protocollo, non inviare altri messaggi e distruggere tutti i
dati specifici del protocollo. Per esempio, in questo protocollo Alice dovrebbe distruggere x e Y se l’ultima serie di controlli fallisce. Si rimanda al Paragrafo 13.5.5 per una
discussione dettagliata di come gestire questi fallimenti.
Questo protocollo descrive una variante sicura di DH, ma non va utilizzato in questa
forma esatta. È necessario sottoporre ad hash il risultato k prima che sia utilizzato dal
resto del sistema. Si rimanda al Paragrafo 14.6 per una discussione più dettagliata.

11.9 Che cosa può andare male?
Pochi libri o articoli sottolineano l’importanza di controllare che i numeri ricevuti
appartengano al sottogruppo corretto. Niels Ferguson per primo rilevò questo problema nel protocollo IKE (Internet Key Exchange) di IPsec [60]. Alcuni dei protocolli IKE
comprendono uno scambio DH. Poiché IKE deve operare nel mondo reale, deve gestire
situazioni con messaggi persi, perciò specifica che, se Bob non riceve risposta, deve inviare
di nuovo il suo ultimo messaggio. IKE non specifica come Alice deve elaborare il messaggio inviato nuovamente da Bob. Ed è facile per Alice commettere un grave errore.
Per semplicità supponiamo che Alice e Bob utilizzino il protocollo DH nel sottogruppo illustrato nella Figura 11.3 senza controllare che X e Y siano valori corretti. Inoltre,
dopo questo scambio Alice inizia a utilizzare la nuova chiave k per inviare un messaggio

Diffie-Hellman   175

cifrato e autenticato a Bob, contenente altri dati del protocollo (è una situazione molto
comune, e casi simili possono presentarsi in IKE).
Ecco il comportamento pericoloso di Alice: quando riceve un nuovo invio del secondo
messaggio contenente Y, Alice si limita a ricalcolare la chiave k e a inviare la risposta
appropriata a Bob. Sembra tutto giusto, vero? Ma ora può entrare in gioco l’attaccante
Eve. Sia d un divisore piccolo (p − 1). Eve può sostituire Y con un elemento di ordine
d. La chiave k di Alice ora è limitata a d valori possibili, ed è interamente determinata
da Y e (x mod d). Eve prova tutti i possibili valori di (x mod d), calcola la chiave k che
Alice avrebbe ottenuto e tenta di decifrare il successivo messaggio che Alice invia. Se
Eve determina correttamente (x mod d), questo messaggio sarà decifrato correttamente,
e così Eve ha ottenuto (x mod d).
E se p − 1 contiene un numero di piccoli fattori (d1, d2, ... , dk)? Allora Eve può eseguire questo
attacco ripetutamente per ciascuno di questi fattori e determinare (x mod d1), ... , (x mod dk).
Utilizzando la forma generale del teorema cinese del resto (cfr. Paragrafo 12.2) Eve può
combinare queste conoscenze per ottenere (x mod d1 d2 d3 ··· dk). Perciò, se il prodotto
di tutti i piccoli divisori di p − 1 è grande, Eve può ottenere una significativa quantità di
informazioni su x. Poiché x dovrebbe essere segreto, questo è sempre un evento sgradito.
In questo caso particolare, Eve può finire per inoltrare ad alice l’Y originale e lasciare
che Alice e Bob completino il protocollo; ma Eve ha raccolto sufficienti informazioni
su x da poter trovare la chiave k utilizzata da Alice e Bob.
Per essere chiari, questo non è un attacco verso IKE, ma verso un’implementazione di
IKE che è consentita dallo standard [60]. Tuttavia, a nostro parere il protocollo dovrebbe
contenere informazioni sufficienti perché un programmatore competente possa creare
un’implementazione sicura. Lasciare che circolino informazioni di questo tipo è pericoloso,
perché qualcuno potrebbe eseguire l’implementazione nel modo sbagliato (non abbiamo
verificato se questo attacco sia possibile anche con le più recenti versioni di IKE).
Affinché questo attacco funzioni, Eve deve avere la fortuna di avere un p − 1 con divisori
piccoli a sufficienza. Stiamo lavorando contro un avversario in grado di eseguire 2128
passaggi di elaborazione. Questo consente a Eve di trarre vantaggio da tutti i divisori
di p − 1 fino a 2128. Non conosciamo una buona analisi probabilistica della quantità di
informazioni che Eve potrebbe ottenere, ma una rapida stima indica che in media Eve
sarà in grado di ottenere approssimativamente 128 bit di informazioni da x dai fattori
minori di 2128. Potrà poi attaccare la parte sconosciuta di x utilizzando un attacco di
collisione, e x è lunga solo 256 bit, perciò si ha un attacco reale. O almeno lo sarebbe se
non controllassimo che X e Y appartengano al sottogruppo corretto.
L’attacco diventa ancora più semplice se Eve è la persona che ha scelto il sottogruppo
(p, q, g). In questo caso potrebbe lei stessa aver inserito i piccoli divisori in p − 1. O
forse Eve fa parte del comitato che ha raccomandato l’adozione di certi parametri per
uno standard. Non è una follia come sembra. Il governo statunitense, tramite il NIST,
fornisce numeri primi che possono essere utilizzati con DSA, uno schema di firma che
utilizza sottogruppi. Altri enti dello stesso governo statunitense (per esempio NSA, CIA,
FBI) hanno interesse a poter violare le comunicazioni private. Non vogliamo certamente
concludere che i numeri primi forniti siano “maligni”, ma è utile un controllo prima di
utilizzarli. Non è difficile farlo, infatti il NIST ha pubblicato un algoritmo per scegliere
parametri che non inserisce fattori piccoli aggiuntivi, e si può controllare se tale algoritmo è stato utilizzato. Ma poche persone lo fanno.

176   Capitolo 11

Alla fine, la soluzione più semplice è quella di controllare che ogni valore ricevuto appartenga al sottogruppo corretto.Tutti gli altri modi per contrastare gli attacchi a piccoli
sottogruppi sono molto più complicati. Potreste tentare di individuare direttamente i
piccoli fattori di p − 1, ma è troppo complicato. Potreste richiedere che la persona che
ha generato l’insieme di parametri fornisca la scomposizione in fattori di p − 1, ma
così si complicherebbe notevolmente l’intero sistema. Verificare che i valori ricevuti
appartengano al sottogruppo corretto richiede un po’ di lavoro, ma è la soluzione più
semplice e affidabile.

11.10 Esercizi
Esercizio 11.1 Supponete che 200 persone vogliano comunicare in sicurezza utilizzando chiavi simmetriche, una chiave per ogni coppia di persone. Quanti chiavi simmetriche
utilizzerebbe questo sistema in totale?
Esercizio 11.2 Quali sono i sottogruppi generati da 3, 7 e 10 nel gruppo moltiplicativo
degli interi modulo p = 11?
Esercizio 11.3 Perché un numero r è un quadrato modulo p, p = 2q + 1 e p e q sono
entrambi primi, se e solo se rq = 1 (mod p)?
Esercizio 11.4 Quali problemi potrebbero sorgere (se ve ne sono) se Alice utilizza gli
stessi x e gx per tutte le sue comunicazioni con Bob e Bob utilizza gli stessi y e gy per
tutte le sue comunicazioni con Alice?
Esercizio 11.5 Alice e Bob vorrebbero accordarsi su una chiave AES a 256 bit. Stanno
tentando di decidere se utilizzare chiavi pubbliche DH gx e gy a 256 bit, 512 bit o di altra
lunghezza. Che cosa consigliereste?

Capitolo 12

RSA

RSA è probabilmente il sistema crittografico a chiave
pubblica più diffuso al mondo. Fornisce firme digitali
e cifratura a chiave pubblica, perciò è uno strumento
molto versatile, e si basa sulla difficoltà di scomporre
in fattore numeri grandi, un problema che affascina
molte persone da millenni ed è stato studiato in
lungo e in largo.

12.1 Introduzione
RSA è in qualche modo simile a Diffie-Hellman
(cfr. Capitolo 11), ma tuttavia molto diverso. DiffieHellman (DH) si basa su una funzione unidirezionale:
supponendo che p e g siano note al pubblico, si può
calcolare (gx mod p) a partire da x, ma non si può
calcolare x a partire da gx mod p. RSA si basa su una
funzione unidirezionale trapdoor. Date le informazioni note al pubblico n ed e, è facile calcolare me
mod n a partire da m, ma non nell’altra direzione.
Tuttavia, se si conosce la fattorizzazione di n, allora
è facile eseguire il calcolo inverso. La fattorizzazione di n costituisce l’informazione trapdoor: se la si
conosce, si è in grado di invertire la funzione; se
non la si conosce, non si è in grado di farlo. Questa
funzionalità di trapdoor consente di utilizzare RSA
sia per la cifratura, sia per le firme digitali. Gli autori
di RSA sono Ronald Rivest, Adi Shamir e Leonard
Adleman, che lo pubblicarono per la prima volta nel
1978 [105].

Sommario
12.1 Introduzione
12.2 Il teorema cinese
del resto
12.3 La moltiplicazione
modulo n
12.4 Definizione di RSA
12.5 I pericoli dell’utilizzo
di RSA
12.6 Cifratura
12.7 Firme
12.8 Esercizi

Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

178   Capitolo 12

In tutto questo capitolo utilizzeremo i valori p, q e n. I valori p e q sono numeri primi
grandi e diversi, ognuno con lunghezza dell’ordine di un migliaio di bit o più. Il valore
n è definito da n := pq (è un prodotto normale, non “modulo qualcosa”).

12.2 Il teorema cinese del resto
Anziché eseguire calcoli modulo un numero primo p come nel sistema DH, in questo
capitolo eseguiremo calcoli modulo il numero composto n. Per spiegare che cosa ci
attende dobbiamo introdurre ancora un po’ di teoria dei numeri sui calcoli modulo n.
Uno strumento molto utile è il teorema del resto cinese o CRT (Chinese Remainder Theorem),
così chiamato perché la versione di base fu sviluppata per la prima volta dal matematico
cinese del terzo secolo Sun Tzu (gran parte della matematica necessaria per DH e RSA
risale a migliaia di anni fa, perciò non può essere troppo difficile, vero?).
I numeri modulo n sono 0, 1, ... , n − 1, e non formano un campo finito come avviene
quando n è primo. I matematici indicano l’insieme di questi numeri con Zn e lo chiamano anello, ma questa terminologia non ci servirà.
Per ogni x in Zn, possiamo calcolare la coppia (x mod p, x mod q). Il teorema cinese del
resto afferma che si può calcolare la funzione inversa: conoscendo (x mod p, x mod q)
si può risalire a x.
Per facilità di notazione definiamo (a, b) := (x mod p, x mod q).
Per prima cosa mostriamo che risalire a x è possibile, poi forniremo un algoritmo per
calcolare x. Per essere in grado di calcolare x dati (a, b), dobbiamo assicurarci che non
esista un altro numero x’ in Zn tale che x' mod p = a e x' mod q = b. Se così fosse, x and x'
porterebbero alla stessa coppia (a, b), e nessun algoritmo sarebbe in grado di determinare
quali di questi due numeri fosse l’input originale.
Sia d := x − x' la differenza tra i numeri che portano alla stessa coppia (a, b). Abbiamo (d mod p) = (x − x') mod p = (x mod p) − (x' mod p) = a − a = 0; quindi d è un
multiplo di p. Sostanzialmente per lo stesso motivo, d è un multiplo di q. Questo implica
che d è un multiplo di mcm(p, q), perché mcm è il minimo comune multiplo. Poiché p e
q non numeri primi diversi, mcm(p, q) = pq = n, quindi x − x' è un multiplo di n. Sia x
che x' sono compresi nell’intervallo 0, ... , n − 1, perciò x − x' deve essere un multiplo
di n compreso nell’intervallo −n + 1, ... , n − 1. L’unica soluzione valida è x − x' = 0, o
x = x'. Questo prova che per ogni coppia (a, b) esiste al più una soluzione per x. Tutto
ciò che dobbiamo fare è trovarla.

12.2.1 La formula di Garner
Il modo più pratico per calcolare la soluzione è la formula di Garner:
x = (((a − b)(q−1 mod p)) mod p) · q + b
Qui il termine (q−1 mod p) è una costante che dipende soltanto da p e q. Ricordate che
possiamo dividere modulo p, quindi possiamo calcolare (1/q mod p), che è semplicemente
un modo diverso di scrivere (q−1 mod p).
Non è necessario capire la formula di Garner. Tutto ciò che ci serve è dimostrare che
il risultato x è corretto.

RSA   179

Per prima cosa mostriamo che x è compreso nell’intervallo 0, ... , n − 1. Naturalmente x
≥ 0. La parte t := (((a − b)(q−1 mod p)) mod p) deve essere compresa nell’intervallo 0,
... , p − 1 perché è un risultato modulo p. Se t ≤ p − 1, allora tq ≤ (p − 1)q e x = tq + b
≤ (p − 1)q + (q − 1) = pq − 1 = n − 1. Questo mostra che x è compreso nell’intervallo
0, ... , n − 1.
Il risultato dovrebbe essere corretto anche modulo p e modulo q.
x mod q = ((((a − b)(q−1 mod p)) mod p) · q + b) mod q

= (K · q + b) mod q
per qualche K

= b mod q

=b
Tutto quanto precede la moltiplicazione per q è un certo intero K, ma qualsiasi multiplo
di q è irrilevante nel calcolo modulo q. Il caso del modulo p è un po’ più complicato:
x mod p = ((((a − b)(q−1 mod p)) mod p) · q + b) mod p

= (((a − b)q−1) · q + b) mod p

= ((a − b)(q−1 q) + b) mod p

= ((a − b) + b) mod p

= a mod p

=a
Nella prima riga non facciamo che espandere (x mod p). Nella riga seguente eliminiamo
un paio di operatori mod p ridondanti. Poi cambiamo l’ordine delle moltiplicazioni,
cosa che non cambia il risultato (ricordate che la moltiplicazione è associativa, perciò
(ab)c = a(bc)). Al passo successivo osserviamo che q−1 q = 1 (mod p), perciò possiamo
eliminare questo termine. Il resto è banale.
Questa procedura è un po’ più complessa di quelle che abbiamo visto finora, soprattutto per il maggior uso di proprietà algebriche. Non preoccupatevi se non riuscite a
seguirla.
Possiamo concludere che la formula di Garner fornisce un risultato x nell’intervallo
corretto e per cui (a, b) = (x mod p, x mod q). Poiché sappiamo già che può esistere
un’unica soluzione, la formula di Garner risolve del tutto il problema del teorema cinese
del resto.
Nei sistemi reali, generalmente si precalcola il valore q−1 mod p, perciò la formula di
Garner richiede una sottrazione modulo p, una moltiplicazione modulo p, una moltiplicazione normale e un’addizione.

12.2.2 Generalizzazioni
Il teorema cinese del resto vale anche quando n è il prodotto di più numeri primi tutti
diversi (esistono versioni valide quando n è divisibile per il quadrato o potenze superiori
di alcuni numeri primi, ma sono ancora più complesse). La formula di Garner può essere
generalizzata a questi casi, ma non ci serve per gli scopi di questo libro.

180   Capitolo 12

12.2.3 Impieghi
A che cosa può servire il teorema cinese del resto? Se avete la necessità di eseguire molti
calcoli modulo n, l’utilizzo del teorema cinese del resto fa risparmiare parecchio tempo.
Per un numero 0 ≤ x < n, chiamiamo la coppia (x mod p, x mod q) la rappresentazione CRT di x. Se abbiamo x e y in rappresentazione CRT, allora la rappresentazione
CRT di x + y è ((x + y) mod p, (x + y) mod q), facile da calcolare a partire dalle
rappresentazioni CRT di x e y. Il primo componente (x + y) mod p si può calcolare
come ((x mod p) + (y mod p) mod p). Questa è semplicemente la somma (modulo p)
della prima metà di ciascuna delle rappresentazioni CRT. Il secondo componente del
risultato si può calcolare in modo simile.
Si può calcolare una moltiplicazione praticamente nello stesso modo. La rappresentazione
CRT di xy è (xy mod p, xy mod q), facile da calcolare a partire dalle rappresentazioni CRT
di x e y. La prima parte (xy mod p) si calcola moltiplicando (x mod p) e (y mod p) e riducendo il risultato modulo p. La seconda parte si calcola nello stesso modo, modulo q.
Sia k il numero di bit di n. Ognuno dei numeri primi p e q è lungo circa k/2. Una
addizione modulo n richiederebbe un’addizione a k bit, eventualmente seguita da una
sottrazione a k bit qualora il risultato superasse n. Nella rappresentazione CRT si devono eseguire due addizioni modulo su numeri di dimensione dimezzata. La quantità di
lavoro è più o meno la stessa.
Nel caso della moltiplicazione, il teorema cinese del resto fa risparmiare parecchio tempo.
Moltiplicare due numeri di k bit richiede molto più lavoro che moltiplicare due volte
due numeri di k/2 bit. Per la maggior parte delle implementazioni, la moltiplicazione
CRT è doppiamente più veloce di una moltiplicazione normale. Il risparmio di tempo
è significativo.
Per l’elevamento a potenza, il teorema cinese del resto consente un risparmio ancora
maggiore. Supponete di dover calcolare xs mod n. L’esponente s può essere lungo fino
a k bit. L’operazione richiede circa 3k/2 moltiplicazioni modulo n. Utilizzando la rappresentazione CRT, ogni moltiplicazione richiede meno lavoro, ma c’è anche un altro
risparmio. Vogliamo calcolare (xs mod p, xs mod q). Nel calcolo modulo p, possiamo
ridurre l’esponente s modulo (p − 1), e analogamente modulo q. Perciò dobbiamo calcolare soltanto (xs mod (p−1) mod p, xs mod (q−1) mod q). Ognuno degli esponenti è lungo
soltanto k/2 bit e richiede soltanto 3k/4 moltiplicazioni. Anziché 3k/2 moltiplicazioni
modulo n, eseguiamo 2 · 3k/4 = 3k/2 moltiplicazioni modulo uno dei due numeri
primi. In un’implementazione tipica si ottiene un risparmio di tempo di elaborazione
di un fattore 3 – 4.
I soli costi portati dall’utilizzo del teorema cinese del resto sono la maggiore complessità
del software e le necessarie conversioni. Se eseguite numerose moltiplicazioni, il carico
di lavoro per le conversioni è ben compensato. Nella maggior parte dei libri si parla
soltanto del teorema cinese del resto come tecnica di implementazione per RSA, noi
invece riteniamo che la rappresentazione CRT faciliti notevolmente la comprensione
del sistema RSA, ed è per questo che abbiamo scelto di trattarla prima. Tra breve la
utilizzeremo per spiegare il comportamento del sistema RSA.

RSA   181

12.2.4 Conclusione
In conclusione: un numero x modulo n può essere rappresentato come una coppia (x
mod p, x mod q) quando n = pq. La conversione tra le due rappresentazioni è abbastanza
semplice. La rappresentazione CRT è utile se si devono svolgere molte moltiplicazioni
modulo un numero composto di cui si conosce la fattorizzazione (non potete utilizzarla
per velocizzare i calcoli se non conoscete la fattorizzazione di n).

12.3 La moltiplicazione modulo n
Prima di addentrarci nei dettagli di RSA, dobbiamo esaminare il comportamento dei
numeri modulo n con la moltiplicazione, che è un po’ diverso dal caso del modulo p
discusso in precedenza.
Per qualsiasi numero primo p, sappiamo che per ogni 0 < x < p vale l’equazione xp−1 =
1 (mod p). Questo non è vero quando si lavora modulo un numero composto n. Perché
RSA funzioni, dobbiamo trovare un esponente t tale che xt = 1 mod n per (quasi) ogni
x. Nella maggior parte dei libri ci si limita a fornire la soluzione, che non aiuta a capire
perché è così. In realtà è relativamente facile trovare la soluzione corretta utilizzando il
teorema cinese del resto.
Vogliamo un t tale che, per quasi tutti gli x, xt = 1 (mod n). Quest’ultima equazione
implica che xt = 1 (mod p) e xt = 1 (mod q). Poiché p e q sono entrambi primi, questo
vale solo se p − 1 è un divisore di t e q − 1 è un divisore di t. Il più piccolo t che gode
di questa proprietà è quindi mcm(p − 1, q − 1) = (p − 1)(q − 1)/mcd(p − 1, q − 1). Per
il resto di questo capitolo utilizzeremo la convenzione per cui t = mcm(p − 1, q − 1).
Le lettere p, q e n sono utilizzate da tutti, anche se alcuni utilizzano le maiuscole. Nella
maggior parte dei libri non si utilizza t, ma la funzione totiente di Eulero φ(n). Per un n
della forma n = pq, la funzione totiente di Eulero si può calcolare come φ(n) = (p − 1)
(q − 1), che è un multiplo del nostro t. È certamente vero che xφ(n) = 1, e che utilizzando
φ(n) al posto di t si ottengono soluzioni corrette, ma l’uso di t è più preciso.
Nella nostra discussione abbiamo saltato un piccolo aspetto: xt mod p non può essere
uguale a 1 se x mod p = 0. Perciò l’equazione xt mod n = 1 non può valere per tutti i
valori di x. Esistono alcuni numeri (non molti) che soffrono di questo problema; sono
q numeri con x mod p = 0 e p numeri con x mod q = 0, perciò il numero totale di
valori che presentano questo problema è p + q. O per la precisione p + q − 1, perché
abbiamo contato due volte il valore 0. Si tratta di una frazione significativa del numero
totale di valori n = pq. Ancora meglio, la proprietà effettiva utilizzata da RSA è che xt+1
= x (mod n), e vale anche per questi numeri speciali. Ancora, è facile da vedere quando
si utilizza la rappresentazione CRT. Se x = 0 (mod p), allora xt+1 = 0 = x (mod p), e
analogamente modulo q. La proprietà fondamentale xt+1 = x (mod n) è preservata e vale
per tutti i numeri in Zn.

182   Capitolo 12

12.4 Definizione di RSA
Ora possiamo definire il sistema RSA. Iniziamo scegliendo a caso due numeri primi grandi
e diversi, p e q, e calcoliamo n = pq. I numeri primi p e q dovrebbero avere dimensione
(quasi) uguale, e il modulo n avrà dimensione doppia rispetto a essi.
Utilizziamo due diversi esponenti, tradizionalmente denominati e e d. Il requisito è che
ed = 1 (mod t) dove t := mcm(p − 1, q − 1) come prima. Ricordiamo che in molti testi
si scrive ed = 1 (mod φ(n)). Scegliamo come esponente pubblico e un piccolo valore
dispari e utilizziamo la funzione extendedGCD del Paragrafo 10.3.5 per calcolare d
come inverso di e modulo t. Ciò garantisce che ed = 1 (mod t).
Per cifrare un messaggio m, il mittente calcola il testo cifrato c := me (mod n). Per decifrare
un testo cifrato c, il ricevente calcola cd (mod n), che è uguale a (me)d = med = mkt+1 =
(mt)k · m = (1)k · m = m (mod n), dove k è un certo valore esistente. Perciò il ricevente
può decifrare il testo cifrato me per ottenere il testo in chiaro m.
La coppia (n, e) forma la chiave pubblica; questi valori vengono tipicamente distribuiti
a molte parti interessate. I valori (p, q, t, d) sono la chiave privata e vengono mantenuti
segreti da parte di chi ha generato la chiave RSA.
Per comodità, spesso scriviamo c1/e mod n anziché cd mod n. Gli esponenti di un calcolo modulo n sono tutti presi modulo t, perché xt = 1 (mod n), perciò i multipli di
t nell’esponente non influenzano il risultato. Abbiamo calcolato d come l’inverso di e
modulo t, perciò scrivere d come 1/e è naturale. La notazione c1/e è spesso più facile da
seguire, soprattutto quando sono in uso più chiavi RSA; ecco perché parliamo anche
di prendere la e-esima radice di un numero. Ricordate soltanto che i calcoli delle radici
modulo n richiedono la conoscenza della chiave privata.

12.4.1 Firme digitali con RSA
Finora abbiamo parlato soltanto di cifratura di messaggi con RSA, ma uno dei grandi
vantaggi di questo sistema è che può essere usato anche per firmare i messaggi. Queste
due operazioni utilizzano gli stessi calcoli. Per firmare un messaggio m, il proprietario
della chiave privata calcola s := m1/e mod n. La coppia (m, s) ora è un messaggio firmato. Per verificare la firma, chiunque conosca la chiave pubblica può verificare che
se = m (mod n).
Come per la cifratura, la sicurezza della firma si basa sul fatto che la radice e-esima di m
può essere calcolata soltanto da qualcuno che conosca la chiave privata.

12.4.2 Gli esponenti pubblici
La procedura descritta finora presenta un problema. Se e ha un fattore comune con
t = mcm(p − 1, q − 1), non esiste soluzione per d. Perciò dobbiamo scegliere p, q ed
e in modo che questa situazione non si verifichi. Si tratta più di un fastidio che di un
problema reale, ma occorre affrontarlo.
La scelta di un esponente pubblico piccolo rende RSA più efficiente, poiché servono
meno calcoli per elevare un numero alla potenza e. Quindi cerchiamo di scegliere un
valore piccolo per e. In questo libro sceglieremo un valore fissato per e, e sceglieremo p
e q in modo da soddisfare le condizioni appena descritte.

RSA   183

Dovete prestare attenzione al fatto che le funzioni di cifratura e quelle di firma digitale non
interagiscano in modi indesiderati. Non deve essere possibile per un attaccante decifrare
un messaggio c convincendo il proprietario della chiave privata a firmare c. Dopo tutto,
firmare il “messaggio” c è la stessa operazione utilizzata per decifrare il testo cifrato c. Le
funzioni di codifica presentate più avanti nel libro consentiranno di evitare ciò. Queste
codifiche sono relativamente simili alle modalità di funzionamento dei cifrari a blocchi;
non dovete utilizzare direttamente le operazioni RSA di base. In ogni caso, non vogliamo comunque utilizzare la stessa operazione RSA per entrambe le funzioni. Potremmo
utilizzare chiavi RSA diverse per la cifratura e l’autenticazione, ma così aumenterebbe
la complessità e raddoppierebbe la quantità di materiale della chiave.
Un altro approccio, quello che adottiamo qui, prevede l’utilizzo di due diversi esponenti
pubblici sullo stesso n. Utilizzeremo e = 3 per le firme ed e = 5 per la cifratura. Così si
separano i sistemi, perché la radice cubica e la radice quinta modulo n sono indipendenti
tra loro: conoscerne una non aiuta l’attaccante a calcolare l’altra [46].
La scelta di valori fissi per e semplifica il sistema e fornisce prestazioni prevedibili. Impone
una restrizione sui numeri primi utilizzabili, perché p − 1 e q − 1 non possono essere
multipli di 3 o 5. È comunque facile verificare tale condizione quando si generano i
numeri primi.
Il motivo alla base della scelta di 3 e 5 è semplice: sono i più piccoli valori adatti allo
scopo (in linea di principio si potrebbe utilizzare e = 2, ma così si introdurrebbero altre complicazioni). Scegliamo l’esponente pubblico più piccolo per le firme, perché le
forme spesso vengono verificate molte volte, mentre ogni porzione di dati viene cifrata
una volta sola, quindi ha senso fare in modo che la verifica della firma sia l’operazione
più rapida.
Altri valori comuni utilizzati per e sono 17 e 65537. Preferiamo i valori più piccoli,
perché sono più efficienti. La scelta di valori piccoli per gli esponenti pubblici comporta
alcuni problemi secondari, che però elimineremo più avanti con le nostre funzioni di
codifica.
Sarebbe bello avere anche un valore piccolo per d, ma in questo caso dobbiamo deludervi. Benché sia possibile trovare una coppia (e, d) con d piccolo, l’utilizzo di un valore
piccolo per d non è sicuro [127]. Non è dunque il caso di rischiare scegliendo un valore
troppo piccolo.

12.4.3 La chiave privata
È estremamente difficile per l’attaccante trovare uno dei valori della chiave privata p, q,
t o d se conosce soltanto la chiave pubblica (n, e). Se n è sufficientemente grande, non
esiste un algoritmo noto in grado di fare ciò in un tempo accettabile. La soluzione
migliore per quanto ne sappiamo è quella di fattorizzare n in p e q, quindi calcolare t e
d da essi. Ecco perché si sente spesso dire che la fattorizzazione è così importante per
la crittografia.
Abbiamo parlato della chiave privata costituita dai valori p, q, t e d. Risulta che la conoscenza di uno qualsiasi di questi valori è sufficiente per calcolare tutti e tre gli altri.Vale
la pena di capire perché.
Supponiamo che l’attaccante conosca la chiave pubblica (n, e), dato che si tratta di un’informazione pubblica. Se conosce p o q, il resto è facile. Dato il valore di p può calcolare
q = n/p, e poi calcolare t e d come abbiamo fatto in precedenza.

184   Capitolo 12

E se l’attaccante conosce (n, e, t)? Innanzitutto t = (p − 1)(q − 1)/mcd(p − 1, q − 1),
ma dato che (p − 1)(q − 1) è molto vicino a n, è facile trovare mcd(p − 1, q − 1) che è
l’intero più vicino a n/t (il valore mcd(p − 1, q − 1) non è mai molto grande, perché è
molto improbabile che due numeri casuali condividano un fatto grande).
Questo consente all’attaccante di calcolare (p − 1)(q − 1). Può anche calcolare n − (p − 1)
(q − 1) + 1 = pq − (pq − p − q + 1) + 1 = p + q. Ora quindi conosce sia n = pq che
s := p + q. Può dunque ricavare le seguenti equazioni:





s = p + q
s = p + n/p
ps = p2 + n
0 = p2 − ps + n

L’ultima è semplicemente un’equazione quadratica in p che si può risolvere con calcoli
da scuola media superiore. Naturalmente, una volta che l’attaccante conosce p, può
calcolare anche tutti gli altri valori della chiave privata.
Qualcosa di simile accade se l’attaccante conosce d. In tutti i nostri sistemi e sarà molto
piccolo. Dato che d < t, il numero ed − 1 è dato da un fattore piccolo per t. L’attaccante
può semplicemente indovinare questo fattore, calcolare t e poi provare a trovare p e q
come prima. Se fallisce, basta che provi le altre possibilità (esistono tecniche più veloci,
ma questa è facile da capire).
In breve, la conoscenza di uno qualsiasi dei valori p, q, t o d consente all’attaccante di calcolare tutti gli altri. È quindi ragionevole assumere che il proprietario della chiave privata
abbia tutti e quattro i valori. Le implementazioni devono semplicemente memorizzare
uno di questi valori, ma spesso invece memorizzano più valori tra quelli necessari per
svolgere l’operazione di decifratura RSA. Questo fatto dipende dall’implementazione e
non ha rilievo dal punto di vista della crittografia.
Se Alice vuole decifrare o firmare un messaggio, deve ovviamente conoscere d. Poiché
conoscere d è equivalente a conoscere p e q, possiamo tranquillamente assumere che
conosca i fattori di n e possa quindi utilizzare la rappresentazione CRT per i suoi calcoli. Questo è utile, perché l’elevamento di un numero alla potenza d è l’operazione
più costosa in RSA, e l’uso della rappresentazione CRT consente di risparmiare lavoro
per un fattore 3 – 4.

12.4.4 La dimensione di n
Il modulo n dovrebbe avere la stessa dimensione del modulo p che si utilizzerebbe nel
caso di Diffie-Hellman. Si rimanda al Paragrafo 11.7 per i dettagli. A titolo di ripasso:
la dimensione minima assoluta per n è di circa 2048 bit se si vogliono proteggere i dati
per 20 anni. Questo valore minimo aumenta lentamente al crescere della velocità dei
computer. Se potete permettervelo, utilizza come n un valore lungo 4096 bit, o della
lunghezza più vicina a questa che siate in grado di gestire. Inoltre, assicuratevi che il vostro software supporti valori di n lunghi fino a 8192 bit. Non si sa mai che cosa porterà
il futuro, e potrebbe essere vitale poter passare all’uso di chiavi più lunghe senza dover
sostituire il software o l’hardware.
I due numeri primi p e q dovrebbero avere la stessa dimensione. Per un modulo n di k
bit potete semplicemente generare due numeri primi casuali di k/2 bit e moltiplicarli;
potreste ottenere un modulo n di k − 1 bit, ma non è poi così importante.

RSA   185

12.4.5 Generazione di chiavi RSA
Per mettere insieme il tutto presentiamo due routine che generano chiavi RSA con le
proprietà desiderate. La prima è una versione modificata della funzione generateLargePrime riportata nel Paragrafo 10.4. L’unica modifica funzionale è che richiediamo che
il numero primo soddisfi p mod 3 ≠ 1 e p mod 5 ≠ 1 per assicurarci di poter utilizzare
gli esponenti pubblici 3 e 5. Naturalmente, se volete utilizzare un diverso valore fisso
per e, dovete modificare opportunamente questa routine.
function generateRSAPrime
input: k Dimensione del numero primo desiderato, espressa come numero di bit.
output: p Numero primo casuale nell’intervallo 2k−1, ... , 2k − 1
con p mod 3 ≠ 1 ^ p mod 5 ≠ 1.

Verifica che il valore rientri in un intervallo.

assert 1024 ≤ k ≤ 4096

Calcola il massimo numero di tentativi.

r ← 100k

repeat

r←r−1

assert r > 0

Sceglie n come numero casuale di k bit.

n ∈ℜ 2k−1, ... , 2k − 1

Prosegue con i tentativi fino a trovare un numero primo.

until n mod 3 ≠ 1 ^ n mod 5 ≠ 1 ^ isPrime(n)

return n
Anziché specificare un intervallo completo in cui il numero primo dovrebbe ricadere,
specifichiamo soltanto la dimensione del numero primo. È una definizione meno flessibile ma in un certo senso più semplice, ed è sufficiente per RSA. I requisiti in più sono
nella condizione del ciclo. Un’implementazione migliore non richiamerà mai isPrime(n)
se n non è adatto modulo 3 o 5, poiché isPrime può richiedere una quantità di calcoli
significativa.
E allora perché continuiamo a inserire il contatore del ciclo con la condizione di errore?
Ora che l’intervallo è sufficientemente grande, troveremo sempre un numero primo
adatto? Speriamo di sì, ma succedono sempre strane cose. Non siamo preoccupati di
ottenere un intervallo che non contenga alcun numero primo, ma ci preoccupa la possibilità di un PRNG compromesso che restituisca lo stesso risultato composto. Questo
è, sfortunatamente, un problema comune dei generatori di numeri casuali, e questo
semplice controllo protegge generateRSAPrime da PRNG che si comportino in modo
errato. Un altro possibile problema è dato da una funzione isPrime compromessa che
indichi sempre che il numero è composto. Naturalmente, se una di queste funzioni non
si comporta come dovrebbe, significa che abbiamo anche problemi ben più seri di cui
preoccuparci.

186   Capitolo 12

La funzione seguente genera tutti i parametri della chiave.
function generateRSAKey
input: k   Dimensione del modulo in numero di bit.
output: p, q   Fattori del modulo.

n   Modulo di circa k bit.

d3   Esponente per la firma.

d5   Esponente per la cifratura.

Verifica che il valore rientri in un intervallo.

assert 2048 ≤ k ≤ 8192

Genera i numeri primi.

p ← generateRSAPrime(k/2)

q ← generateRSAPrime(k/2)

Un piccolo test per il caso in cui il PRNG sia stato compromesso...

assert p ≠ q

Calcola t come mcm(p − 1, q − 1).

t ← (p − 1)(q − 1)/gcd(p − 1, q − 1)

Calcola gli esponenti segreti utilizzando la funzionalità di inversione modulare dell’algoritmo
extendedGCD.

g, (u, v) ← extendedGCD(3, t)

Verifica che il massimo comun divisore sia corretto, altrimenti non otterremo un inverso.

assert g = 1

Riduce u modulo t, poiché u potrebbe essere negativo e d3 non deve esserlo.

d3 ← u mod t

E ora tocca a d5.

g, (u, v) ← extendedGCD(5, t)

assert g = 1

d5 ← u mod t

return p, q, pq, d3, d5
Notate che abbiamo utilizzato le scelte fisse per gli esponenti pubblici e che generiamo
una chiave che può essere utilizzata sia per la firma (e = 3) sia per la cifratura (e = 5).

12.5 I pericoli dell’utilizzo di RSA
Utilizzare RSA come descritto finora è molto pericoloso. Il problema sta nella struttura
matematica. Per esempio, se Alice firma digitalmente due messaggi m1 e m2, allora Bob
può calcolare la firma di Alice su m3 := m1m2 mod n. Dopo tutto, Alice ha calcolato m11/e
e m21/e e Bob può moltiplicare i due risultati per ottenere (m1m2)1/e.
Un altro problema nasce se Bob cifra un messaggio molto piccolo m con la chiave pubblica di Alice. Se e = 5 e m < 5 n , allora me = m5 < n, perciò non viene mai effettuata la
riduzione modulare. L’attaccante Eve può determinare m semplicemente calcolando la

RSA   187

radice quinta di m5, cosa facile perché non ci sono riduzioni modulari. Una situazione
tipica in cui potrebbe presentarsi questo problema è quella in cui Bob tenta di inviare
ad Alice una chiave AES. Se si limita a prendere il valore di 256 bit come un intero,
allora la chiave cifrata è minore di 2256·5 = 21280, quindi molto più piccola del nostro n.
Non c’è mai una riduzione modulare, ed Eve può determinare la chiave semplicemente
calcolando la radice quinta del valore chiave cifrato.
Uno dei motivi che ci hanno spinti a spiegare in dettaglio la teoria alla base di RSA è
l’intenzione di mostrarvi parte della struttura matematica coinvolta; è la stessa struttura
che apre le porte a molti tipi di attacchi, alcuni dei quali sono stati citati nel paragrafo
precedente. Esistono attacchi molto più avanzati, basati su tecniche per la risoluzione di
equazioni polinomiali modulo n.Tutti comunque si riducono a un unico punto: è molto
pericoloso i numeri sui quali RSA opera siano legati da una struttura di qualsiasi tipo.
La soluzione consiste nell’utilizzare una funzione che distrugga qualsiasi struttura presente. Talvolta si parla di funzione di riempimento, ma è un nome sbagliato. Il termine
“riempimento” è normalmente utilizzato per indicare l’inserimento di byte aggiuntivi per
ottenere un risultato della lunghezza desiderata. Diverse persone hanno utilizzato varie
forme di riempimento per la cifratura e la firma RSA, e in alcuni casi questo ha causato
il sorgere di attacchi. In realtà serve una funzione che rimuova quanta più struttura sia
possibile. La chiameremo funzione di codifica.
Per questo scopo esistono degli standard, in particolare PKCS #1 v2.1 [110]. Come al
solito, questo non è un singolo standard. Esistono due schemi di cifratura RSA e due
schemi di firma RSA, ognuno dei quali può utilizzare una varietà di funzioni di hash.
Non è necessariamente un problema, ma dal punto di vista didattico non ci piace tutta
questa complessità, perciò presenteremo alcuni metodi più semplici, anche se potrebbero
non avere tutte le caratteristiche di alcuni dei metodi PKCS. Inoltre, come abbiamo
detto in precedenza nel caso di AES, l’utilizzo di un algoritmo standardizzato presenta
molti vantaggi. Per esempio, per la cifratura potreste utilizzare RSA-OAEP [9] e per le
firme RSA-PSS [8].
Lo standard PKCS #1 v2.1 esibisce un problema comune nella documentazione tecnica:
mescola la specifica con l’implementazione. La funzione di decifratura RSA è specificata
due volte: una utilizzando l’equazione m = cd mod n e un’altra utilizzando equazioni CRT.
Questi due calcoli portano allo stesso risultato: uno è semplicemente un’implementazione ottimizzata dell’altro. Simili descrizioni dell’implementazione non dovrebbero far
parte dello standard, perché non producono comportamenti diversi.Andrebbero discusse
separatamente. Non intendiamo criticare questo standard PKCS in particolare; si tratta
di un problema molto diffuso che si ritrova ovunque nel settore dei computer.

12.6 Cifratura
La cifratura di un messaggio è l’applicazione canonica di RSA, e tuttavia non viene quasi
mai usata nella pratica. Il motivo è semplice: la dimensione del messaggio che può essere
cifrato con RSA è limitata dalla dimensione di n. Nei sistemi del mondo reale non si
possono utilizzare tutti i bit, perché la funzione di codifica ha un sovraccarico. Questa
dimensione limitata non va bene per la maggior parte delle applicazioni, e poiché RSA
è piuttosto costoso in termini computazionali, non è il caso di suddividere un messaggio
in blocchi più piccoli da cifrare uno per uno con un’applicazione separata di RSA.

188   Capitolo 12

La soluzione adottata quasi ovunque è quella di scegliere una chiave segreta casuale K
e cifrarla con le chiavi RSA. Il messaggio effettivo, m, viene poi cifrato con la chiave
K utilizzando un cifrario a blocchi o a flusso. Perciò, invece di inviare qualcosa come
ERSA(m), si invia ERSA(K), EK(m). Così la dimensione del messaggio non è più limitata ed
è richiesta una singola operazione RSA, anche per messaggi grandi. Occorre trasmettere
un po’ di dati in più, ma questo solitamente è un prezzo che merita di essere pagato in
cambio dei vantaggi ottenuti.
Utilizzeremo un metodo di cifratura ancora più semplice. Invece di scegliere un K e
cifrarlo, scegliamo un r ∈ Zn a caso e definiamo la chiave di cifratura bulk K := h(r) per
una funzione di hash h. La cifratura di r si esegue semplicemente elevandolo alla quinta
potenza modulo n (ricordate che utilizziamo e = 5 per la cifratura). Questa soluzione è
semplice e sicura. Poiché r è scelto a caso, non presenta alcuna struttura che possa essere
utilizzata per attaccare la porzione RSA della cifratura. La funzione di hash a sua volta
garantisce che nessuna struttura eventualmente esistente tra diversi r si propaghi in una
struttura tra i K, fatta eccezione per l’ovvio requisito che input uguali debbano portare
a output uguali.
Per semplicità di implementazione scegliamo i nostri r nell’intervallo 0, ... , 2k − 1, dove
k è il massimo numero tale che 2k < n. È più facile generare un numero casuale a k bit
che generare un numero casuale appartenente a Zn, e questa piccola deviazione dalla
distribuzione uniforme non è pericolosa in questa situazione.
Di seguito riportiamo una definizione più formale.
function encryptRandomKeyWithRSA
input: (n, e) Chiave pubblica RSA, nel nostro caso e = 5.
output: K Chiave simmetrica che è stata cifrata.

c Testo cifrato RSA.

Calcola k.

k ← log2 n

Sceglie un r a caso tale che 0 ≤ r < 2k − 1.





r ∈ℜ {0, ... ,2k − 1}
K ← SHAd-256(r)
c ← re mod n
return (K, c)

Il ricevente calcola K = h(c1/e mod n) e ottiene la stessa chiave K.
function decryptRandomKeyWithRSA
input: (n, d) Chiave privata RSA con e = 5.

c Testo cifrato.
output: K Chiave simmetrica che è stata cifrata.

assert 0 ≤ c < n

Questo è banale.

K ← SHAd-256(c1/e mod n)

return K

RSA   189

In precedenza abbiamo spiegato nei dettagli come calcolare c1/e data la chiave privata,
perciò non ripetiamo la discussione qui. Non dimenticate di utilizzare il teorema cinese
del resto per ottenere un risparmio di tempo di un fattore 3 – 4.
Ora presentiamo un buon modo per osservare la sicurezza. Supponiamo che Bob cifri una
chiave K per Alice, e che Eve voglia conoscere qualcosa di questa chiave. Il messaggio di
Bob dipende solo da alcuni dati casuali e dalla chiave pubblica di Alice; perciò, nel caso
peggiore questo messaggio potrebbe far trapelare a Eve dei dati su K, ma non può rivelare
nessun altro segreto, come la chiave privata di Alice. La chiave K è calcolata utilizzando
una funzione di hash, e possiamo ipotizzare che tale funzione sia una mappatura casuale
(se non possiamo trattare la funzione di hash come una mappatura casuale, significa che
non soddisfa il nostro requisito di sicurezza per le funzioni di hash). L’unico modo per
ottenere informazioni sull’output di una funzione di hash è quello di conoscere quanto
più possibile dell’input. Ciò significa che servono informazioni su r. Ma se RSA è sicuro,
e dobbiamo supporre che lo sia, poiché abbiamo scelto di utilizzarlo, allora è impossibile
ottenere una quantità significativa di informazioni su un r scelto a caso conoscendo
soltanto (re mod n). Di conseguenza l’attaccante si trova in grande incertezza riguardo r,
e quindi non ha nessuna conoscenza di K.
Supponiamo che in seguito la chiave K sia rivelata a Eve, per esempio a causa di un
problema di un altro componente del sistema. Questo rivelerebbe qualcosa riguardo la
chiave privata di Alice? No. K è l’output di una funzione di hash, e per Eve è impossibile
ricavare qualsiasi informazione sugli input di tale funzione. Perciò, anche se Eve scegliesse
c in un modo speciale, il K che viene a conoscere non rivela nulla su r. La chiave privata
di Alice è stata utilizzata soltanto per calcolare r, perciò Eve non è in grado di apprendere
nulla nemmeno su di essa.
Questo è uno dei vantaggi dati dalla presenza di una funzione di hash nella funzione
decryptRandomKeyWithRSA. Supponete che sia stato appena restituito il valore c1/e
mod n. Questa routine potrebbe essere utilizzata per fare diversi giochini. Supponete
che qualche altra parte del sistema abbia un punto debole e che Eve abbia ottenuto il
bit meno significativo dell’output. Allora Eve potrebbe inviare ad Alice valori c1, c2, c3,
... scelti appositamente e ottenere i bit meno significativi di c11/e, c21/e, c31/e, ... Questi
risultati presentano proprietà algebriche di tutti i tipi ed è ragionevole pensare che Eve
possa apprendere qualcosa di utile da una situazione come questa. La funzione di hash h
in decryptRandomKeyWithRSA distrugge tutta la struttura matematica. Ottenendo
un solo bit dall’output K, Eve non ha praticamente alcuna informazione su c1/e. Anche
il valore completo di K rivela pochissime informazioni utili. La funzione di hash non è
reversibile. L’aggiunta della funzione di hash in questo punto rende le routine RSA più
sicure contro eventuali vulnerabilità nelle altre parti del sistema.
Questo è anche il motivo per cui decryptRandomKeyWithRSA non verifica che il valore
r calcolato da c rientri nell’intervallo 0, ... , 2k − 1. Se verificassimo questa condizione,
dovremmo gestire l’eventuale errore che potrebbe risultarne. Poiché la gestione degli
errori porta sempre a un comportamento diverso, è probabile che Eve sarebbe in grado
di determinare se questo errore si sia verificato o meno, e così otterrebbe una funzione
che rivela informazioni: Eve potrebbe scegliere un valore c e determinare se c1/e mod
n < 2k . Eve non può calcolare questa proprietà senza l’aiuto di Alice, e non vogliamo
aiutare Eve se possiamo evitarlo. Così, evitando di verificare la condizione, rischiamo al
più di generare un output privo di senso, e questo può accadere in ogni caso, poiché c
potrebbe essere stato corrotto senza dare origine a un valore r non valido (l’imposizione di

190   Capitolo 12

altre restrizioni su r non elimina il problema degli output privi di senso. Eve può sempre
utilizzare la chiave pubblica di Alice e una funzione encryptRandomKeyWithRSA
modificata per inviare ad Alice cifrature di chiavi senza senso).
Una breve digressione: esiste una notevole differenza tra rivelare una coppia casuale (c,
c1/e) e calcolare c1/e per un c scelto da altri. Chiunque è in grado di produrre coppie della
forma (c, c1/e), basta scegliere un r a caso, calcolare la coppia (re, r) e poi impostare c :=
re. Non c’è nulla di segreto su coppie come queste. Tuttavia, se Alice è tanto gentile da
calcolare c1/e per un c ricevuto da Eve, quest’ultima può scegliere valori c con proprietà
speciali, cosa che non può fare per le coppie (c, c1/e) che genera lei stessa. Non fornite
questo servizio a chi vi attacca.

12.7 Firme
Per le firme serve un po’ di lavoro in più. Il problema è che il messaggio m che vogliamo
firmare può contenere una struttura, e non vogliamo che sia presente alcuna struttura
nel numero su cui calcoliamo la radice RSA. Dobbiamo quindi distruggerla.
Il primo passo è quello di sottoporre il messaggio ad hashing. Perciò, al posto di un
messaggio di lunghezza variabile m, abbiamo a che fare con un valore di dimensione fissa
h(m) dove h è una funzione di hash. Se utilizziamo SHAd-256, otteniamo un risultato di
256 bit. Ma n è molto più grande, perciò non possiamo utilizzare h(m) direttamente.
La soluzione è semplice, basta utilizzare una mappatura pseudocasuale per espandere h(m)
in un numero casuale s compreso nell’intervallo 0, ... , n − 1. La firma su m viene poi
calcolata come s1/e (mod n). La mappatura di h(m) su un valore modulo n richiede un
po’ di lavoro (cfr. la discussione svolta nel Paragrafo 9.7). In questa situazione particolare
possiamo tranquillamente semplificare il nostro problema mappando h(m) su un elemento
a caso nell’intervallo 0, ... , 2k − 1, dove k è il massimo numero tale che 2k < n. I numeri
nell’intervallo 0, ... , 2k − 1 si generano facilmente perché ci basta generare k bit casuali.
Nella nostra particolare situazione questa è una situazione sicura, ma non va utilizzata
ovunque. Esistono molte situazioni in cui tale situazione porterebbe a compromettere
l’intero sistema.
Utilizzeremo il generatore descritto per il PRNG Fortuna del Capitolo 9. Molti sistemi
utilizzano la funzione di hash h per costruire un generatore di numeri casuali a questo
scopo specifico, ma ne abbiamo già definito uno adatto. Inoltre, c’è bisogno del PRNG
per scegliere i numeri primi per generare le chiavi RSA, perciò il PRNG è già incluso
nel software.
Si hanno tre funzioni: una per mappare il messaggio in s, una per firmare il messaggio
e una per verificare la firma.
function MsgToRSANumber
input: n Chiave pubblica RSA, modulo.

m Messaggio da convertire in un valore modulo n.
output: s Un numero modulo n.

Crea un nuovo generatore PRNG.

G ← InitializeGenerator()

Fornisce come seme l’hash del messaggio.
Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

RSA   191










ReSeed(G, SHAd-256(m))
Calcola k.
k ← log2 n
x ← PseudoRandomData(G, k/8)
Come di consueto, trattiamo la stringa di byte x come un intero utilizzando la convenzione
del byte meno significativo al primo posto. La riduzione modulare può essere
implementata con un semplice and sull’ultimo byte di x.
s ← x mod 2k
return s

function SignWithRSA
input: (n, d) Chiave privata RSA con e = 3.

m Messaggio da firmare.
output: σ Firma su m.

s ← MsgToRSANumber(n, m)

σ ← s1/e mod n

return σ
La lettera σ, sigma, è utilizzata spesso per le firme perché è l’equivalente in greco della
lettera s (per signature, in inglese “firma”). Ormai dovreste sapere come calcolare s1/e mod
n, data la chiave privata.
function VerifyRSASignature
input: (n, e) Chiave pubblica RSA con e = 3.

m Messaggio che si suppone debba essere firmato.

σ Firma sul messaggio.

s ← MsgToRSANumber(n, m)

assert s = σe mod n
Naturalmente in un’applicazione reale sarà necessario intraprendere qualche azione nel
caso in cui la verifica della firma fallisca. Ci siamo limitati a scrivere un’asserzione per
indicare che non dovrebbe proseguire la normale attività. Il fallimento di una firma
dovrebbe essere considerato come qualsiasi altro fallimento in un protocollo crittografico: come un chiaro segnale che si è sotto attacco. Non inviate alcuna risposta a meno
che non abbiate l’assoluta necessità di farlo, e distruggete tutto il materiale su cui state
lavorando. Più informazioni inviate, più materiale fornite all’attaccante.
Le considerazioni sulla sicurezza per le nostre firme RSA sono simili a quelle svolte
per la cifratura RSA. Se chiedete ad Alice di firmare un gruppo di messaggi m1, m2 , ...
, mi, otterrete coppie della forma (s, s1/e), ma i valori s sono effettivamente casuali. Se la
funzione di hash è sicura, potete intervenire su h(m) soltanto procedendo per tentativi.
Il generatore casuale è ancora una mappatura casuale. Chiunque può creare coppie
della forma (s, s1/e) per valori casuali s, perciò questo non fornisce nuove informazioni
utili all’attaccante per falsificare una firma. Tuttavia, per qualsiasi messaggio m, solo chi
conosce la chiave privata può calcolare la coppia (s, s1/e) corrispondente, perché s deve

192   Capitolo 12

essere calcolato da h(m), e poi s1/e deve essere calcolato da s. Questo richiede la chiave
privata. Quindi, chiunque verifichi la firma sa che è Alice ad averla apposta.
Siamo giunti al termine della nostra trattazione di RSA, e anche al termine della parte
più pesante del libro dal punto di vista della matematica. Utilizzeremo DH e RSA per
il nostro protocollo di negoziazione della chiave e per PKI, ma senza introdurre nuovi
concetti e tecniche di matematica.

12.8 Esercizi
Esercizio 12.1 Sia p = 89, q = 107, n = pq, a = 3 e b = 5. Trovate x in Zn tale che a =
x (mod p) e b = x (mod q).
Esercizio 12.2 Sia p = 89, q = 107, n = pq, x = 1796 e y = 8931. Calcolate x + y (mod
n) direttamente. Calcolate x + y (mod n) utilizzando rappresentazioni CRT.
Esercizio 12.3 Sia p = 89, q = 107, n = pq, x = 1796 e y = 8931. Calcolate xy (mod
n) direttamente. Calcolate xy (mod n) utilizzando rappresentazioni CRT.
Esercizio 12.4 Sia p = 83, q = 101, n = pq ed e = 3. (n, e) è una chiave pubblica RSA
valida? Se sì, calcolate la chiave privata RSA corrispondente d. Se no, perché?
Esercizio 12.5 Sia p = 79, q = 89, n = pq ed e = 3. (n, e) è una chiave pubblica RSA
valida? Se sì, calcolate la chiave privata RSA corrispondente d. Se no, perché?
Esercizio 12.6 Per velocizzare la cifratura,Alice ha scelto di impostare la propria chiave
privata d = 3 e calcola e come l’inverso di d modulo t. È una buona decisione?
Esercizio 12.7 Una chiave RSA a 256 bit (una chiave con un modulo a 256 bit)
fornisce una forza simile a quella di una chiave AES a 256 bit?
Esercizio 12.8 Sia p = 71, q = 89, n = pq ed e = 3. Per prima cosa trovate d. Poi calcolate la firma su m1 = 5416, m2 = 2397 e m3 = m1m2 (mod n) utilizzando le operazioni
di base RSA. Mostrate che la terza firma è equivalente al prodotto delle prime due.

Capitolo 13

Introduzione
ai protocolli
crittografici
I protocolli crittografici consistono di scambi di
messaggi tra i partecipanti. Ne abbiamo già visto uno
semplice nel Capitolo 11.
Creare protocolli sicuri è una sfida. Il problema
principale è costituito dal fatto che il progettista o
l’implementatore non ha il pieno controllo. Finora
abbiamo progettato un sistema, mantenendo il controllo sulle modalità di funzionamento delle varie
parti. Una volta iniziata la comunicazione con altre
parti, non si ha più alcun controllo sulla loro attività.
L’altra parte ha un insieme di interessi diversi, e può
deviare dalle regole stabilite per cercare di ottenere
un vantaggio. Quando si lavora sui protocolli, è necessario partire dal presupposto di avere a che fare
con il nemico.

13.1 I ruoli
I protocolli vengono tipicamente descritti come
eseguiti da Alice e Bob, o un cliente e un fornitore.
Nomi come “Alice,” “Bob,” “cliente,” e “fornitore”
non sono veramente intesi a identificare un individuo
o un’organizzazione in particolare; identificano un
ruolo all’interno del protocollo. Se il signor Smith
vuole comunicare con il signor Jones, può attivare
un protocollo di accordo sulla chiave. Il signor Smith
potrebbe avere il ruolo di Alice, e il signor Jones
quello di Bob. Il giorno successivo i ruoli potrebbero essere invertiti. È importante tenere presente
che un’entità singola può rivestire uno qualsiasi dei
ruoli (nei protocolli con tre o più partecipanti, è
anche possibile che una singola persona rivesta più

Sommario
13.1 I ruoli
13.2 La fiducia
13.3 Gli incentivi
13.4 La fiducia nei
protocolli crittografici
13.5 Messaggi e attività
13.6 Esercizi

194   Capitolo 13

ruoli contemporaneamente.). Ciò è particolarmente importante quando si analizza il
protocollo per la sicurezza.Abbiamo già visto l’attacco man-in-the-middle sul protocollo
DH. In quell’attacco, Eve si appropria dei ruoli di Alice e Bob (naturalmente Eve riveste
anche un altro ruolo.)

13.2 La fiducia
La fiducia è la base fondamentale per tutti i rapporti che intratteniamo con altre persone. Se non si ha assolutamente fiducia in nessuno e in nulla, perché preoccuparsi di
interagire con gli altri? Per esempio, l’acquisto di una barretta di cioccolato presuppone
un livello base di fiducia. Il cliente deve aver fiducia che il commerciante gli fornisca il
dolce e gli dia il resto corretto. Il commerciante deve aver fiducia che il cliente paghi.
Entrambi vengono tutelati se l’altra parte si comporta in maniera scorretta. Il furto nei
negozi viene perseguito. I commercianti truffatori rischiano cattiva pubblicità, azioni
legali, e magari un pugno sul naso.
Esistono diverse fonti di fiducia.
• Etica.  L’etica ha una grande influenza nella nostra società. Anche se pochissime
persone, se non nessuna, si comportano sempre in modo etico, la maggior parte si
comporta eticamente per la gran parte del tempo. I trasgressori sono pochi. La maggior parte delle persone paga per i propri acquisti, anche quando sarebbe facilissimo
appropriarsene rubandoli.
• Reputazione. Avere un “buon nome” è molto importante nella nostra società. Gli
individui e le aziende desiderano proteggere la propria reputazione. Spesso la minaccia
di cattiva pubblicità costituisce un incentivo a comportarsi in maniera corretta.
• Legge. Nelle società evolute esiste un’infrastruttura normativa che supporta i procedimenti legali e il perseguimento delle persone che compiono azioni scorrette.
Anche questo è un incentivo a mantenere comportamenti adeguati.
• Minaccia fisica. Un altro sprone in questo senso è costituito dalla paura di danni
fisici in caso di illeciti che vengano scoperti. Questa è una delle fonti della fiducia
per i trafficanti di droga e altri commerci illegali.
• MAD.  Si tratta di un acronimo che ricorda la Guerra Fredda: Mutually Assured
Destruction, distruzione reciproca assicurata. In forme più blande, è la prospettiva di
farsi male e di far male agli altri. Se una persona inganna un’amica, quest’ultima
potrebbe rompere l’amicizia, con dolore per entrambi. A volte si vedono aziende
in situazioni “MAD”, soprattutto quando intraprendono procedimenti legali l’una
contro l’altra per violazioni di brevetto.
In tutti i casi si tratta di meccanismi per cui una parte è incentivata a non truffare. L’altra
parte conosce il meccanismo, e per questo può in qualche misura fidarsi del proprio
antagonista. Ecco perché tutti questi incentivi falliscono quando si tratta con persone
completamente irrazionali: non ci si può fidare del fatto che agiscano secondo i loro
migliori interessi, e ciò annulla tutti questi meccanismi.
È difficile sviluppare fiducia in Internet. Supponiamo che Alice viva all’estero e si connetta al sito web di ACME. ACME non ha quasi nessun motivo per fidarsi di Alice; tra i
meccanismi che abbiamo citato, rimane soltanto l’etica. La tutela legale nei confronti di
individui privati che vivono all’estero è quasi impossibile, e comporta costi proibitivi nella

Introduzione ai protocolli crittografici    195

maggior parte dei casi. Non è possibile danneggiare in maniera efficace la reputazione
degli individui o minacciarli, nemmeno con azioni “MAD”.
Esiste tuttavia una base di fiducia tra Alice e ACME, perché ACME ha una reputazione
da difendere. È importante ricordarlo quando si progetta un protocollo per il commercio
elettronico. Se ci sono modalità di eventi negativi (e ci sono sempre), l’evento negativo
dovrebbe essere a carico di ACME, perché essa ha un incentivo a risolvere la questione
in maniera appropriata con un intervento manuale (quasi tutti gli scambi commerciali
via telefono, posta, strumenti elettronici nei confronti di singoli individui seguono questa
regola in cui il cliente paga per l’ordine prima che venga soddisfatto). Se invece è a danno
di Alice, è meno probabile che la questione venga risolta in maniera appropriata. Inoltre,
ACME sarà vulnerabile agli attacchi che cercano di indurre la modalità di fallimento
per poi trarne vantaggio.
La fiducia non è una questione di principio. Non si tratta di fidarsi o meno di qualcuno.
La fiducia viene concessa a persone diverse con gradi diversi. È possibile fidarsi di un
conoscente per 100 euro, ma non per il proprio biglietto della lotteria che ha appena
vinto il premio da 5 milioni di euro. Ci fidiamo delle banche per tenere al sicuro il
nostro denaro, ma conserviamo ricevute e copie di assegni annullati perché non ci fidiamo completamente della loro gestione. La domanda: “Ti fidi di lui?” non è completa.
Dovrebbe essere: “Ti fidi di lui per la faccenda X?”

13.2.1 Rischio
La fiducia è fondamentale negli affari, tuttavia solitamente si parla in termini di rischio
piuttosto che di fiducia. Il rischio può essere considerato come l’opposto della fiducia.
I rischi vengono valutati, confrontati e trattati in varie forme.
Quando si lavora su protocolli crittografici, è più semplice esprimersi in termini di
fiducia che in termini di rischi. Tuttavia, la mancanza di fiducia è semplicemente un
rischio, che in alcuni casi può essere gestito mediante tecniche standard di gestione del
rischio come le procedure assicurative. Si parla di fiducia quando si progettano i protocolli. Occorre tenere presente che gli operatori pensano e parlano sempre in termini di
rischio. È necessario adattarsi a entrambe le prospettive per essere in grado di dialogare
con tutte le parti.

13.3 Gli incentivi
La struttura degli incentivi è un altro elemento fondamentale per qualsiasi analisi di un
protocollo. Quali sono gli obiettivi dei diversi partecipanti? Che cosa vorrebbero arrivare a ottenere? Anche nella vita reale, analizzare la struttura degli incentivi consente di
formulare conclusioni di grande interesse.
Parecchie volte la settimana consultiamo rassegne stampa in cui si annunciano notizie
del tipo: “Una nuova ricerca ha dimostrato che...”. La nostra prima reazione è sempre
quella di chiedere chi ha finanziato la ricerca. Le ricerche i cui risultati sono favorevoli
alla parte che le ha finanziate risultano sempre sospette. Diversi fattori entrano in gioco
in questi casi. Innanzitutto, i ricercatori sanno quello che i loro clienti vogliono sentirsi
dire, e sanno che possono ottenere contratti aggiuntivi se producono “buoni” risultati.
Ciò introduce un elemento di parzialità. In secondo luogo, lo sponsor della ricerca non è

196   Capitolo 13

intenzionato a pubblicare risultati negativi di qualsiasi tipo. Il fatto di pubblicare soltanto
i risultati positivi introduce un altro elemento negativo. Le aziende produttrici di tabacco
pubblicarono risultati “scientifici” sul fatto che la nicotina non creasse dipendenza. Microsoft finanzia ricerche che “provano” il fatto che il software open source è in qualche
modo deleterio. Mai fidarsi di ricerche che supportano l’azienda che le ha finanziate.
Noi stessi siamo abituati a pressioni di questo tipo. Negli anni passati come consulenti,
abbiamo effettuato numerose valutazioni della sicurezza per vari committenti. Spesso
eravamo severi (il prodotto medio che valutavamo era decisamente di pessimo livello)
e le nostre valutazioni presentavano elementi significativamente negativi. Ciò non ci ha
resi molto popolari tra i nostri clienti. Un committente ha persino chiamato Bruce per
dirgli: “Interrompi il lavoro e mandami il conto. Ho trovato una persona che chiede
meno e scrive report migliori”. Avete indovinato che cosa si intende per “migliore” in
questo caso?
Possiamo riscontrare esattamente lo stesso problema in altri campi. Quando abbiamo
scritto la prima edizione di questo libro, la stampa era piena di notizie che riguardavano
aziende finanziarie e bancarie. Analisti e contabili redigevano report favorevoli per i
loro clienti, anziché valutazioni eque. La struttura di incentivi è stata la spinta negativa
che ha portato questi professionisti a orientare in maniera parziale le loro valutazioni.
Osservare come funzionano gli incentivi è molto istruttivo, e noi ce ne siamo occupati
per anni. Con un po’ di pratica è sorprendentemente semplice, e conduce a risultati
molto apprezzabili.
Se si sceglie di remunerare i dirigenti con stock option, si adotta una struttura di incentivi del
tipo: se aumenta il valore nei successivi tre anni ricaverai una fortuna, se diminuisce, otterrai
una bella stretta di mano. Si tratta dello schema: “Testa vinco tanto, croce vinco poco”,
quindi indovinate che cosa fanno alcuni dirigenti? Scelgono una strategia ad alto rischio
e a breve termine. Se ci sarà l’opportunità di raddoppiare la quota investita, riusciranno
sempre a coglierla, perché raccoglieranno solo la vincita e non rifonderanno la perdita. Se
hanno la possibilità di gonfiare il prezzo delle azioni per qualche anno con trucchi nella
tenuta dei libri contabili, lo faranno, perché potranno comunque incassare prima che il
raggiro venga scoperto. Alcune speculazioni falliscono, ma altre valgono la candela.
Qualcosa di analogo successe con il settore del risparmio e del prestito negli Stati Uniti
negli anni ’80. Il governo federale deregolamentò il settore, consentendo di investire più
liberamente le somme raccolte. Contemporaneamente il governo garantiva i depositi.
Osserviamo la struttura degli incentivi: se gli investimenti rendono, l’azienda ottiene un
profitto, e senza dubbio la dirigenza un buon premio; se gli investimenti sono in perdita,
il governo federale rifonde gli investitori. Non fu una sorpresa che un gruppetto di
aziende arrivò a perdere moltissimi soldi su investimenti ad alto rischio – e il governo
federlale pagò il conto.
Spesso correggere la struttura di incentivi è relativamente semplice. Per esempio, anziché
affidare alle aziende la retribuzione delle agenzie di revisione contabile, potrebbe essere
la Borsa a occuparsene. Se si stabilisce un premio significativo per ogni errore riscontrato
dai revisori contabili, si otterranno revisioni decisamente più accurate.
Gli esempi di strutture di incentivi indesiderabili abbondano. Gli avvocati divorzisti sono
incentivati a rendere molto conflittuale il divorzio, poiché vengono pagati per ogni ora
spesa a combattere per le proprietà. Si può tranquillamente scommettere che l’avvocato
consiglierà di interrompere le ostilità non appena la parcella rischi di superare il valore
della proprietà contesa.

Introduzione ai protocolli crittografici    197

Nella società americana, le cause legali sono comuni. Se si verifica un problema, ogni
parte ha una forte incentivo a nascondere, negare o comunque evitare di prendersi la
colpa. Una normativa rigorosa sulla responsabilità civile e i cospicui risarcimenti dei
danni possono sembrare ottimi provvedimenti per la società, inizialmente, ma ostacolano
notevolmente la nostra capacità di analizzare quello che è successo per evitarlo in futuro.
Le norme concepite per proteggere i consumatori rendono praticamente impossibile
per un’azienda come Firestone (per esempio) ammettere l’esistenza di un problema nei
propri prodotti in modo che tutti possano capire come produrre pneumatici migliori.
I protocolli crittografici interagiscono in due modi con le strutture di incentivi. In primo
luogo, si basano su strutture di incentivi. Alcuni protocolli di pagamento elettronico non
impediscono ai commercianti di frodare il cliente, ma sono in grado di fornire prova
della frode al cliente. Ciò avviene perché il protocollo crea un tracciato crittografico. Il
commerciante, quindi, ha un incentivo a non trovarsi ad affrontare clienti che possano
dimostrare di essere stati truffati. La prova potrebbe essere utilizzata in tribunale o anche
soltanto per compromettere la reputazione del venditore.
In secondo luogo, i protocolli crittografici modificano la struttura degli incentivi. Rendono impossibili determinate azioni, eliminandole dalla struttura degli incentivi. Possono
anche aprire nuove possibilità e nuovi incentivi. Se si utilizza un conto bancario online,
si crea automaticamente un incentivo per un potenziale ladro che potrebbe introdursi
nel sistema e sottrarre denaro utilizzando questo metodo.
Inizialmente gli incentivi sembrano essere soprattutto di tipo materiale, ma questi rappresentano soltanto una parte. Molte persone hanno motivazioni diverse. Nelle relazioni
personali, gli incentivi più importanti hanno poco a che vedere con il denaro. Cercate
di allargare le vostre vedute e di comprendere che cosa stimoli le persone. Poi create i
vostri protocolli tenendo conto di ciò.

13.4 La fiducia nei protocolli crittografici
La funzione dei protocolli crittografici è quella di ridurre al minimo la dose di fiducia
necessaria per operare. È un concetto talmente importante che va ribadito. La funzione
dei protocolli crittografici è quella di ridurre al minimo la dose di fiducia necessaria
per operare. Ciò significa ridurre al minimo sia il numero di persone che devono fidarsi
l’una dell’altra, sia la dose di fiducia che devono avere.
Un potente strumento per creare protocolli crittografici è il modello della paranoia. Quando Alice partecipa a un protocollo, parte dal presupposto che tutte le altre parti coinvolte
siano in combutta per ingannarla. Questa è proprio la teoria estrema della cospirazione.
Naturalmente, ciascuno dei partecipanti parte dal medesimo presupposto. Questo è il
modello di default con cui vengono concepiti tutti i protocolli crittografici.
Qualsiasi allontanamento da questo modello di default deve essere accuratamente documentato. È sorprendente come questa fase venga così spesso trascurata. A volte vediamo
dei protocolli utilizzati in situazioni in cui non è presente la necessaria fiducia. Per esempio, la maggior parte dei siti web utilizza il protocollo SSL. Questo protocollo richiede
dei certificati, che tuttavia sono facili da ottenere. Il risultato è che l’utente comunica in
maniera sicura con un sito web, ma non sa con quale sito web sta comunicando. Molti
episodi di phishing perpetrati contro utenti di PayPal hanno portato alla luce proprio
questa vulnerabilità, per esempio.

198   Capitolo 13

È molto facile farsi tentare dal non documentare l’affidabilità richiesta per un particolare
protocollo, perché spesso è “ovvia”. Ciò potrebbe essere vero per il creatore del protocollo, ma come per qualsiasi modulo del sistema, anche il protocollo dovrebbe avere
un’interfaccia chiaramente specificata, per tutti i motivi consueti.
Dal punto di vista aziendale, i requisiti di affidabilità documentati elencano anche i rischi.
Ciascun punto implica un rischio che deve essere considerato.

13.5 Messaggi e attività
Una tipica descrizione di protocollo è costituita da un certo numero di messaggi che
vengono inviati tra le parti e una descrizione delle operazioni che deve effettuare ciascun
partecipante.
Praticamente tutte le descrizioni di protocolli sono redatte a un livello molto elevato. La
maggior parte dei dettagli non viene descritta. Ciò consente di focalizzare l’attenzione
sulla funzionalità centrale del protocollo, ma comporta anche un grande pericolo: senza specifiche accurate di tutte le azioni che ciascun partecipante deve intraprendere, è
estremamente difficile creare un’implementazione sicura del protocollo.
Talvolta si vedono protocolli specificati in tutti i più piccoli dettagli e controlli. Tali
specifiche sono spesso così complesse che nessuno riesce a comprenderle appieno. Ciò
potrebbe essere di aiuto a un implementatore, ma qualsiasi elemento troppo complesso
non può essere molto sicuro.
La soluzione, come sempre, è quella di modulare il livello di dettaglio a seconda delle
esigenze. Nei protocolli crittografici, come nei protocolli di comunicazione, è possibile
suddividere le funzionalità richieste in diversi livelli di protocollo. Ciascun livello opera
al di sopra del livello precedente.Tutti i livelli sono importanti, ma la maggior parte sono
uguali per tutti i protocolli. Soltanto il protocollo superiore è molto diverso, ed è quello
che deve sempre essere documentato.

13.5.1 Il livello di trasporto
Gli specialisti delle reti ci perdoneranno per l’utilizzo, nel nostro contesto, di uno dei
termini tipici delle reti. Per chi si occupa di crittografia, il livello di trasporto è il sistema
di comunicazione che consente alle parti di comunicare tra loro. Si tratta di stringhe
di byte che vengono inviate da un partecipante all’altro. Il modo in cui ciò avviene è
irrilevante per il nostro scopo. Ciò di cui i crittografi devono preoccuparsi è la possibilità
di inviare una stringa di byte da un partecipante all’altro. Si possono utilizzare pacchetti
UDP, un flusso di dati TCP, posta elettronica o qualsiasi altro metodo. In molti casi il
livello di trasporto necessita di codifiche ulteriori. Per esempio, se un programma esegue
protocolli multipli simultaneamente, il livello di trasporto deve consegnare il messaggio
all’esecuzione del protocollo corretta. Ciò potrebbe richiede un campo di destinazione
aggiuntivo. Quando si utilizza un protocollo TCP, la lunghezza del messaggio deve essere
inclusa per fornire servizi orientati al messaggio sul protocollo TCP orientato ai flussi.
Per essere chiari, si presuppone che il livello di trasporto trasmetta qualsiasi tipo di stringhe di byte. Qualsiasi valore di byte può essere presente nel messaggio. La lunghezza

Introduzione ai protocolli crittografici    199

della stringa è variabile. La stringa ricevuta, naturalmente, deve essere identica alla stringa
inviata; non è consentita la cancellazione dei byte di valore zero finali, o qualsiasi altra
modifica. Alcuni livelli di trasporto comprendono elementi come costanti magiche per
fornire uno strumento di rilevamento precoce degli errori, oppure per controllare la
sincronizzazione del flusso TCP. Se la costante magica non è corretta in un messaggio
ricevuto, il resto del messaggio dovrebbe essere scartato.
Esiste un importante caso speciale.Talvolta si esegue un protocollo crittografico attraverso
un canale crittograficamente sicuro come quello che abbiamo progettato nel Capitolo 7.
In casi come questo, il livello di trasporto fornisce anche riservatezza, autenticazione e
protezione dai tentativi di replica. Ciò rende il protocollo molto più facile da progettare,
perché ci sono molti meno tipi di attacchi di cui preoccuparsi.

13.5.2 Identità del protocollo e del messaggio
Il livello successivo definisce gli identificatori del protocollo e del messaggio. Quando si
riceve un messaggio, si vuole sapere a quale protocollo appartiene e di quale messaggio
all’interno del protocollo si tratta.
L’identificatore del protocollo contiene tipicamente due parti. La prima parte è l’informazione che riguarda la versione, in cui c’è spazio per gli aggiornamenti successivi.
La seconda parte identifica il protocollo crittografico a cui appartiene il messaggio. In
un sistema di pagamento elettronico potrebbero esserci dei protocolli per operazioni di
revoca, pagamento, deposito, rimborso e così via. L’identificatore del protocollo evita
che si crei confusione tra messaggi di protocolli diversi.
L’identificatore del messaggio indica di quale dei messaggi del protocollo in questione
si tratta. Se ci sono quattro messaggi in un protocollo, non è auspicabile che vi sia alcun
tipo di confusione sull’identità dei messaggi.
Perché è necessario includere così tante informazioni di identificazione? Un malintenzionato non potrebbe falsificarle tutte? Certamente potrebbe farlo. Questo livello
non fornisce alcuna protezione contro azioni di contraffazione, ma consente di rilevare
errori accidentali. È importante disporre di buoni strumenti di riconoscimento degli
errori accidentali. Supponete di essere responsabili del mantenimento di un sistema:
improvvisamente ricevete un gran numero di messaggi di errore. È importante essere
in grado di distinguere attacchi in atto da errori accidentali come problemi relativi alla
configurazione e alla versione.
Gli identificatori di protocollo e di messaggio rendono il messaggio più compatto, il
che è utile per facilitare le procedure di manutenzione e debugging. Le automobili e gli
aerei sono progettati per una facile manutenzione. Il software è persino più complesso,
motivo in più per progettarlo in modo che sia facile da manutenere.
Probabilmente la ragione principale per includere informazioni di identificazione del
messaggio ha a che fare con il principio di Horton. Quando si utilizzano strumenti di
autenticazione (o una firma digitale) in un protocollo, generalmente si autenticano diversi
campi di messaggio e dati. Inserendo informazioni di identificazione del messaggio, si
evita il rischio che esso venga interpretato nel contesto sbagliato.

200   Capitolo 13

13.5.3 Codifica e analisi del messaggio
Il livello successivo è quello della codifica. Ogni elemento di dati del messaggio deve
essere convertito in una sequenza di byte. Si tratta di un problema di programmazione
standard e non entreremo nei dettagli in questo contesto.
Un punto particolarmente importante è la fase di analisi o parsing. Il ricevente deve essere
in grado di analizzare il messaggio, che ha l’aspetto di una sequenza di byte, determinando i suoi campi costitutivi. Questa analisi non deve dipendere dalle informazioni
del contesto.
Un campo di lunghezza fissa che è uguale in tutte le versioni del protocollo è facile da
analizzare. Si sa esattamente quanto è lungo. I problemi iniziano quando le dimensioni
o il significato di un campo dipendono da qualche informazione di contesto, come
messaggi precedenti nel protocollo. Così si apre la strada a possibili guai.
Molti messaggi nei protocolli crittografici terminano con una firma o un altro tipo di
autenticazione. La funzione di autenticazione certifica una stringa di byte, e solitamente
è più semplice autenticare il messaggio sul livello di trasporto. Se l’interpretazione di
un messaggio dipende da qualche informazione di contesto, la firma o autenticazione è
ambigua. Parecchi protocolli vengono compromessi a causa di questo tipo di errore.
Un buon modo per codificare i campi è quello di utilizzare sistemi di cifratura TLV
(Tag-Length-Value). Ciascun campo viene codificato in tre elementi di dati. L’etichetta
identifica il campo, la lunghezza è la lunghezza della codifica del valore, il valore è il
dato effettivo che deve essere codificato. La più nota codifica TLV è ASN.1 [64], ma è
incredibilmente complessa, perciò non la trattiamo. Un sottoinsieme di ASN.1 potrebbe
essere molto utile.
Un’altra alternativa è XML. Dimenticate l’aspetto conosciuto di XML; lo utilizziamo
soltanto come un sistema di codifica dei dati. Finché si utilizza un DTD (Document
Template Definition) fissato, l’analisi non dipende dal contesto e non si verificherà alcun
problema.

13.5.4 Stati di esecuzione del protocollo
In molte implementazioni, un singolo computer può prendere parte a diverse esecuzioni
di protocollo contemporaneamente. Per tenere traccia di tutti i protocolli è necessaria una
qualche forma di indicazione dello stato di esecuzione del protocollo. Lo stato contiene
tutte le informazioni necessarie per completare l’esecuzione del protocollo.
L’implementazione dei protocolli necessita di qualche tipo di programmazione a eventi,
poiché l’esecuzione deve attendere che arrivino i messaggi esterni, prima di proseguire.
Ciò si può ottenere in vari modi, per esempio utilizzando un thread o processo per
ogni esecuzione del protocollo, oppure con qualche tipo di sistema di distribuzione
degli eventi.
Data un’infrastruttura per la programmazione a eventi, implementare un protocollo è
relativamente semplice. Lo stato del protocollo contiene un meccanismo di indicazione
del tipo di messaggio atteso. Come regola generale, nessun altro tipo di messaggio è
accettabile. Se il tipo di messaggio atteso arriva, viene analizzato ed elaborato secondo
le regole.

Introduzione ai protocolli crittografici    201

13.5.5 Errori
I protocolli contengono sempre una moltitudine di controlli, tra cui la verifica del tipo
di protocollo e del tipo di messaggio, il controllo che si tratti del tipo di messaggio atteso per lo stato di esecuzione del protocollo, l’analisi del messaggio, e l’esecuzione delle
verifiche crittografiche specificate. Se qualcuno di questi controlli fallisce, significa che
si è incontrato un errore.
Gli errori devono essere gestiti molto attentamente, perché rappresentano una potenziale
via di accesso per gli attacchi. La procedura più sicura è quella di non inviare nessuna
risposta all’errore e cancellare immediatamente lo stato del protocollo. In questo modo
si riducono al minimo le informazioni che un malintenzionato potrebbe ottenere. Purtroppo ciò rende il sistema poco intuitivo, poiché non vi è nessuna traccia dell’errore.
Per rendere i sistemi utilizzabili, spesso occorre aggiungere messaggi di errore di qualche
tipo. Se ne avete la possibilità, fate in modo di non inviare un messaggio di errore alle
altre parti del protocollo. Registrate un messaggio di errore su un log sicuro in modo
che l’amministratore di sistema possa risolvere il problema. Se proprio dovete inviare un
messaggio di errore, fate in modo che fornisca meno informazioni possibile. Un semplice:
“Si è verificato un errore” è spesso sufficiente.
Un’interazione pericolosa è quella tra errori e attacchi basati sul tempo. Eve potrebbe
inviare un messaggio fasullo ad Alice e attendere la sua risposta all’errore. Il tempo che
impiega Alice per rilevare l’errore e inviare una risposta spesso consente di ottenere
informazioni dettagliate sull’errore e sul punto in cui si è verificato.
Ecco un buon esempio dei pericoli di queste interazioni.Anni fa, Niels Ferguson lavorava
su un sistema di smart card commerciale. Una delle caratteristiche era il codice PIN che
era necessario per abilitare la carta. Il codice PIN a quattro cifre veniva inviato alla carta,
la quale rispondeva con un messaggio che indicava se era stata abilitata o meno. Se questo
sistema fosse stato implementato correttamente, sarebbero stati necessari 10.000 tentativi
per esaurire tutti i codici PIN possibili. La smart card consentiva cinque tentativi errati
di inserimento del PIN prima di bloccarsi, dopodiché occorreva uno speciale sistema
di sblocco con altri mezzi. L’idea era che un malintenzionato che non conoscesse il
codice poteva fare cinque tentativi per indovinarlo, ovvero aveva 1 probabilità su 2000
di azzeccare il codice PIN prima che la carta si bloccasse.
Il sistema era buono, e schemi analoghi vengono ampiamente impiegati anche oggi.
Una possibilità di 1 su 2000 è adeguata per molte applicazioni. Sfortunatamente, però, il
programmatore di quel particolare sistema di smart card prese una decisione problematica. Per verificare il codice PIN a quattro cifre, il programma inizialmente controllava
la prima cifra, successivamente la seconda e così via. La carta riportava il fallimento
del tentativo di inserire il codice non appena rilevava che una delle cifre era sbagliata.
Il punto debole stava nel fatto che il tempo necessario perché la smart card inviasse il
messaggio di errore “PIN errato” dipendeva da quante cifre del PIN erano corrette. Un
attaccante brillante poteva misurare il tempo e ottenere molte informazioni. In particolare, poteva scoprire in quale posizione si trovava la prima cifra sbagliata. Grazie a questa
conoscenza, gli sarebbero bastati circa 40 tentativi per analizzare in maniera esaustiva lo
spazio del PIN (dopo 10 tentativi avrebbe determinato la prima cifra giusta, dopo altri
10 la seconda e così via). Dopo cinque tentativi, le possibilità di scoprire il codice PIN
corretto salivano a 1 su 143. Si tratta di una situazione molto più favorevole rispetto alla
possibilità di 1 su 2000 prevista dal sistema. Con 20 tentativi, la possibilità saliva al 60%,
molto di più dello 0,2% previsto.

202   Capitolo 13

Peggio ancora, ci sono alcune situazioni in cui fare 20 o 40 tentativi non è impossibile.
Le smart card che si bloccano dopo un certo numero di tentativi di inserimento del
PIN falliti resettano sempre il conteggio una volta che viene utilizzato il PIN corretto,
quindi l’utente ottiene altri cinque tentativi di digitazione del codice PIN corretto la
volta successiva. Supponete che il vostro compagno di stanza possieda una smart card
come quella descritta qui. Se siete in grado di mettere le mani sulla sua smart card,
potete effettuare uno o due tentativi prima di rimetterla a posto. Poi aspettate che lui
utilizzi veramente la carta per qualcosa, inserendo il codice PIN corretto e resettando il
contatore dei tentativi di inserimento falliti della smart card. Ora potete fare uno o due
tentativi nuovi. Presto arriverete a scoprire l’intero codice PIN, perché bastano al più
40 tentativi per scoprirlo.
La gestione degli errori è troppo complessa per poter fornire un semplice gruppo di
regole. La nostra comunità non ne sa ancora abbastanza.Al momento, il consiglio migliore
che possiamo dare è quello di essere molto cauti e rivelare il minor numero possibile
di informazioni.

13.5.6 Attacchi con replica e nuovi tentativi
Un attacco con replica avviene quando l’attaccante registra un messaggio e successivamente lo reinvia. Occorre proteggersi contro i messaggi replicati. Possono essere un po’
difficili da rilevare, perché il messaggio appare esattamente come un messaggio veritiero.
E, dopo tutto, lo è.
Strettamente correlato all’attacco con replica è quello con nuovi tentativi. Supponete che
Alice sia impegnata in un protocollo con Bob, e non ottiene risposta. Potrebbero esserci
molti motivi, ma uno comune è che Bob non ha ricevuto l’ultimo messaggio di Alice e
lo sta ancora aspettando. Ciò accade di continuo nella vita reale, e viene risolto inviando
un’altra lettera o messaggio di posta elettronica, o rimandando l’ultimo già inviato. Nei
sistemi automatizzati si parla di “nuovo tentativo”. Alice reinvia il suo ultimo messaggio
a Bob e aspetta di nuovo la risposta.
Bob può ricevere repliche di messaggi inviati da attaccanti e messaggi inviati nuovamente
da Alice. Deve quindi gestirli in maniera adeguata e assicurare modalità di comportamento
corrette senza introdurre vulnerabilità dal punto di vista della sicurezza.
Inviare messaggi già inviati è relativamente semplice. Ciascun partecipante ha un’indicazione di stato di esecuzione del protocollo.Tutto ciò che occorre fare è misurare il tempo
e inviare nuovamente l’ultimo messaggio se non si riceve risposta entro un tempo ragionevole. L’esatto limite di tempo dipende dall’infrastruttura di comunicazione utilizzata.
Se si utilizzano pacchetti UDP (un protocollo che utilizza direttamente pacchetti IP), ci
sono ragionevoli probabilità che il messaggio vada perso, quindi è necessario un tempo
di reinvio breve, dell’ordine di qualche secondo. Se si inviano i messaggi mediante un
protocollo TCP, qualsiasi dato non ricevuto correttamente viene reinviato utilizzando i
timeout del protocollo. Non vi è motivo per effettuare un reinvio a livello del protocollo
crittografico, e la maggior parte dei sistemi che utilizzano TCP non lo fa. Ciononostante,
per la restante parte di questa discussione ipotizzeremo che i tentativi di reinvio vengano
utilizzati, poiché le tecniche generali di gestione dei reinvii funzionano comunque.
Quando si riceve un messaggio, occorre stabilire che cosa farne. Diamo per assunto che
ogni messaggio sia riconoscibile, in modo che si sappia quale messaggio nel protocollo
si presuppone che sia. Se si tratta del messaggio atteso, non c’è niente di diverso dall’or-

Introduzione ai protocolli crittografici    203

dinario e basta semplicemente seguire le regole del protocollo. Supponiamo che si tratti
di un messaggio dal “futuro” del protocollo; ovvero di un messaggio che è atteso soltanto
in una fase temporale successiva. La soluzione è semplice: va ignorato. Non occorre
modificare il proprio stato, inviare una risposta, basta semplicemente lasciarlo cadere e
non fare niente. Probabilmente fa parte di un attacco. Anche in protocolli particolari in
cui potrebbe essere parte di una sequenza di errori indotti da messaggi perduti, ignorare
un messaggio ha lo stesso effetto che si verifica con la perdita del messaggio nel transito.
Poiché il protocollo dovrebbe essere in grado di recuperare i problemi di perdita dei
messaggi, ignorare un messaggio è sempre una soluzione sicura.
Rimane il caso dei “vecchi” messaggi, ovvero dei messaggi già elaborati nel protocollo
che si sta eseguendo. Ci sono tre situazioni in cui ciò potrebbe accadere. Nella prima, il
messaggio che si riceve ha lo stesso identificatore di quello precedente a cui è stata data
risposta, e anche il contenuto è identico a quello del messaggio precedente. In questo caso
il messaggio è probabilmente un “nuovo tentativo”, quindi si procede inviando esattamente la stessa risposta della prima volta. Notate che la risposta deve essere identica; non va
ricalcolata con un valore casuale diverso, e non basta limitarsi a ipotizzare che il messaggio
ricevuto sia identico al precedente a cui si è data risposta: occorre verificarlo.
Il secondo caso riguarda la ricezione di un messaggio con lo stesso identificatore dell’ultimo messaggio a cui si è risposto, ma con un contenuto diverso. Per esempio, supponiamo
che nel protocollo DH Bob riceva il primo messaggio da Alice, e poi successivamente
riceva un altro messaggio che si presenta come il primo, ma contiene dati diversi, superando però i controlli di integrità. Questa situazione indica un attacco. Nessun nuovo
invio potrebbe mai creare questa situazione, poiché il messaggio reinviato non è mai
diverso dal primo tentativo. O il messaggio ricevuto è un falso, o lo è il messaggio precedente a cui è stata data risposta. La scelta sicura è quella di trattare il messaggio come
un errore del protocollo, con tutte le conseguenze di cui abbiamo discusso (ignorare il
messaggio appena ricevuto è una procedura sicura, ma significa che si rileveranno meno
forme di attacco attivo, e ciò ha un effetto deteriore sulle componenti di individuazione
e risposta del sistema di sicurezza).
Il terzo caso si presenta quando si riceve un messaggio che è ancora più vecchio del
messaggio precedente a cui si è risposto. Non c’è molto che si possa fare. Se è stata
mantenuta una copia del messaggio originale ricevuto in quella fase del protocollo, si
può verificare se sia identico a questo. Se lo è, va ignorato. Se è diverso, significa che è
stato individuato un attacco e deve essere trattato come un errore di protocollo. Molte
implementazioni non conservano tutti i messaggi ricevuti nell’esecuzione di un protocollo, e questo rende impossibile sapere se il messaggio ricevuto ora sia o meno identico
a quello elaborato in origine. La strada sicura è quella di ignorare questi messaggi. Queste
situazioni si verificano spesso nella realtà. Talvolta i messaggi vengono ritardati per un
periodo lungo. Supponiamo che Alice invii un messaggio che subisce un ritardo. Dopo
pochi secondi lo reinvia e questo arriva a destinazione, quindi Bob e Alice continuano
con il protocollo. Mezzo minuto più tardi Bob riceve una copia di un messaggio che –
rispetto ai parametri del protocollo – è molto vecchio.
Le cose si complicano ulteriormente se si utilizza un protocollo in cui ci sono più di
due partecipanti. Questi protocolli esistono, ma vanno oltre lo scopo di questo libro.
Se vi capiterà di lavorare su un protocollo multiplo, considerate con molta attenzione i
messaggi replicati e i messaggi reinviati.

204   Capitolo 13

Un commento finale: è impossibile sapere se l’ultimo messaggio di un protocollo è arrivato oppure no. Se Alice invia l’ultimo messaggio a Bob, non otterrà mai una conferma
del suo arrivo. Se il collegamento viene interrotto e Bob non riceve l’ultimo messaggio,
allora Bob riproverà a inviare il messaggio precedente, ma anche questo non raggiungerà
Alice. Per Alice non ha modo di distinguere questa situazione dalla normale conclusione
del protocollo. Si potrebbe aggiungere un riscontro inviato da parte di Bob ad Alice al
termine del protocollo, ma questo riscontro diverrebbe il nuovo ultimo messaggio e
sorgerebbe il medesimo problema. I protocolli crittografici devono essere progettati in
modo che questa ambiguità non porti a comportamenti non sicuri.

13.6 Esercizi
Esercizio 13.1 Descrivete un protocollo che eseguite regolarmente. Potrebbe essere
l’ordinazione di una bevanda in un locale, o la procedura di imbarco su un volo aereo.
Chi sono gli attori effettivi direttamente coinvolti in questo protocollo? Ci sono altri
attori coinvolti marginalmente in questo protocollo, per esempio durante la fase di
configurazione iniziale? Per semplicità, elencate al massimo cinque attori. Create una
matrice in cui ciascuna riga e ciascuna colonna sono etichettate con un attore. Per ogni
cella, descrivete il rapporto fiduciario tra l’attore nella riga e quello nella colonna.
Esercizio 13.2 Considerate la sicurezza del vostro personal computer. Elencate i possibili attaccanti che potrebbero penetrare nel vostro PC, i loro incentivi, e i costi e rischi
associati a ciascuno.
Esercizio 13.3 Ripetete l’Esercizio 13.2, prendendo in esame una banca al posto del
vostro personal computer.
Esercizio 13.4 Ripetete l’Esercizio 13.2, prendendo in esame un computer del Pentagono al posto del vostro personal computer.
Esercizio 13.5 Ripetete l’Esercizio 13.2, prendendo in esame un computer appartenente a un’organizzazione criminale al posto del vostro personal computer.

Capitolo 14

Il protocollo
di negoziazione
della chiave
Siamo finalmente pronti ad affrontare il protocollo di
negoziazione della chiave. Scopo di questo protocollo
è ricavare una chiave condivisa che possa essere utilizzata per il canale sicuro definito nel Capitolo 7.
I protocolli completi sono piuttosto complessi, e
presentare il protocollo finale tutto insieme potrebbe
causare confusione. Presenteremo una sequenza di
protocolli, ognuno dei quali aggiunge alcune funzionalità. Tenete presente che i protocolli intermedi
non sono pienamente funzionali e presentano diversi
punti deboli.
Esistono diversi metodi per progettare il protocollo di
negoziazione della chiave, alcuni supportati da dimostrazioni di sicurezza e altri no.Abbiamo progettato il
nostro protocollo partendo da zero, non solo perché
così si ottiene una spiegazione più chiara, ma anche
perché così abbiamo modo di evidenziare dettagli e
sfide da affrontare a ogni passo della progettazione.

Sommario
14.1 Lo scenario
14.2 Un primo tentativo
14.3 I protocolli sono eterni
14.4 Una convenzione
di autenticazione
14.5 Un secondo tentativo
14.6 Un terzo tentativo
14.7 Il protocollo finale
14.8 Visioni diverse
del protocollo
14.9 Complessità
computazionale
del protocollo
14.10 Complessità
del protocollo

14.1 Lo scenario
Il protocollo prevede due partecipanti: Alice e Bob
vogliono comunicare in modo sicuro. Per prima
cosa eseguiranno il protocollo di negoziazione della
chiave per impostare una chiave di sessione segreta
k, poi utilizzeranno k per creare un canale sicuro su
cui scambiare i dati effettivi.
Per una negoziazione della chiave sicura,Alice e Bob
devono essere in grado di identificarsi a vicenda.
Questa capacità di autenticazione di base è trattata
nella Parte IV di questo libro. Per ora assumeremo
semplicemente che Alice e Bob possano autenticar-

14.11 Un avvertimento
14.12 Negoziazione della
chiave a partire
da una password
14.13 Esercizi

206   Capitolo 14

si i messaggi a vicenda. Questa operazione di autenticazione di base può essere svolta
utilizzando firme RSA (se Alice e Bob conoscono l’uno la chiave dell’altro, o utilizzano
una PKI), o utilizzando una chiave segreta condivisa e una funzione MAC.
Un momento però! Perché eseguire una negoziazione della chiave se abbiamo già una
chiave segreta condivisa? Le ragioni per farlo possono essere molte. Prima di tutto, la
negoziazione della chiave può separare la chiave di sessione dalla chiave condivisa (di
lungo termine) esistente. Se la chiave di sessione viene compromessa (per esempio a
causa di un difetto di implementazione del canale sicuro), la chiave segreta condivisa
rimane sicura. E se la chiave segreta condivisa viene compromessa dopo l’esecuzione
del protocollo di negoziazione della chiave, l’attaccante che determina la chiave segreta
condivisa non conosce ancora la chiave di sessione negoziata dal protocollo. Perciò, i dati
di ieri sono ancora protetti, se si perde la chiave oggi. Queste sono proprietà importanti,
che rendono più robusto l’intero sistema.
Esistono anche situazioni in cui la chiave segreta condivisa è relativamente debole, come
avviene talvolta per una password. Gli utenti non amano memorizzare password di 30
caratteri e tendono a sceglierne di molto più semplici. Un attacco standard è quello di
dizionario, in cui un computer cerca tra un numero elevatissimo di password semplici.
Anche se non li consideriamo in questa sede, alcuni protocolli di negoziazione della
chiave possono trasformare una password debole in una chiave forte.

14.2 Un primo tentativo
Esistono protocolli standard utilizzabili per svolgere la negoziazione della chiave. Uno
ben noto, basato sul protocollo DH, è il protocollo STS (Station-to-Station) [34]. Noi
preferiamo accompagnarvi attraverso la progettazione di un protocollo diverso, a scopo
illustrativo. Iniziamo con il progetto più semplice a cui si possa pensare, quello mostrato
nella Figura 14.1.

Alice
Noto: (p, q, g)
x ∈ 1, . . . , q − 1

Bob
Noto: (p, q, g)
X := g x
Y := g y

k ← Yx

y ∈ 1, . . . , q − 1
k ← Xy

AUTHA (k)
verifica AUTHA (k)
AUTHB (k)
verifica AUTH B (k)
Figura 14.1 Un primo tentativo di negoziazione della chiave.

Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

Il protocollo di negoziazione della chiave    207

È semplicemente il protocollo DH in un sottogruppo con in più un po’ di autenticazione.
Alice e Bob eseguono il protocollo DH utilizzando i primi due messaggi (abbiamo tralasciato alcuni dei controlli necessari per semplicità). Poi Alice calcola un’autenticazione
sulla chiave di sessione k e la invia a Bob, che verifica l’autenticazione. Analogamente,
Bob invia un’autenticazione di k ad Alice.
Non conosciamo la forma esatta dell’autenticazione, al momento. Ricordate che abbiamo
detto di assumere che Alice e Bob siano in grado di autenticarsi i messaggi a vicenda,
perciò Bob è in grado di verificare AuthA(k) e Alice è in grado di verificare AuthB(k).
Che lo facciano utilizzando firme digitali o una funzione MAC non ci interessa qui.
Questo protocollo in sostanza trasforma una capacità di autenticazione in una chiave
di sessione.
Esistono però alcuni problemi, descritti di seguito.
• Il protocollo si basa sull’ipotesi che (p, q, g) siano noti sia ad Alice che a Bob. Scegliere
delle costanti per questi valori è una cattiva idea.
• Utilizza quattro messaggi, mentre sarebbe possibile raggiungere l’obiettivo con tre.
• La chiave di sessione è utilizzata come input per la funzione di autenticazione.
Questo non è un problema se la funzione di autenticazione è forte, ma supponete
che tale funzione lasci trapelare alcuni bit relativi alla chiave di sessione: sarebbe un
male. Si renderebbe certamente necessaria una nuova analisi dell’intero protocollo.
Una buona regola pratica è quella di utilizzare un segreto soltanto per un unico
scopo. In questo caso k sarà utilizzato come chiave di sessione, perciò non vogliamo
utilizzarlo come argomento per la funzione di autenticazione.
• I due messaggi di autenticazione sono troppo simili. Se, per esempio, la funzione di
autenticazione è un semplice MAC che utilizza una chiave segreta nota sia ad Alice
che a Bob, allora Bob potrebbe semplicemente inviare il valore di autenticazione
ricevuto da Alice, e non gli servirebbe la chiave segreta per completare il protocollo.
Quindi Alice non sarebbe convinta dall’ultimo messaggio di autenticazione.
• Nelle implementazioni è necessario prestare attenzione a non utilizzare k finché
sono stati scambiati i messaggi di autenticazione. Questo non è un gran problema,
anzi è un requisito piuttosto semplice, ma non credereste mai che cosa può accadere
talvolta quando si tenta di ottimizzare un programma.
Risolveremo tutti questi problemi in questo capitolo.

14.3 I protocolli sono eterni
Abbiamo messo in evidenza l’importanza di progettare sistemi in grado di resistere al
futuro. Questo è ancora più importante per i protocolli. Se si limita la dimensione dei
campi di database a 2000 byte, potrebbe essere un problema per alcuni utenti, ma potete rimuovere il limite nella prossima versione. Per i protocolli non è così. I protocolli
sono eseguiti tra partecipanti diversi, e ogni versione nuova deve poter interagire con
la precedente. Modificare un protocollo mantenendo la compatibilità con le versioni
precedenti è piuttosto complesso. Prima di poter stabilire se un protocollo è davvero
compatibile, occorre implementarne varie versioni lasciando che sia un sistema a decidere quale utilizzare.

208   Capitolo 14

Il cambio di versione del protocollo diventa naturalmente un punto di attacco. Se un
protocollo più vecchio è meno sicuro, un attaccante è incentivato a spingervi a utilizzare
quello vecchio. Sareste sorpresi di sapere quanti sistemi abbiamo visto affetti dal cosiddetto
attacco di rollback della versione.
Naturalmente è impossibile conoscere tutti i requisiti futuri, perciò a un certo punto
potrebbe essere necessario definire una seconda versione di un protocollo. Tuttavia,
il costo di avere diverse versioni di un protocollo è elevato, soprattutto in termini di
complessità generale.
I protocolli di successo sono quasi eterni (non ci preoccupiamo di quelli che non hanno
successo). È estremamente difficile rimuovere del tutto un protocollo in circolazione,
perciò è ancora più importante progettare protocolli a prova di futuro. Ecco perché
non possiamo specificare un insieme fisso di parametri DH per il nostro protocollo di
negoziazione della chiave. Anche se scegliessimo valori molto grandi, esisterebbe sempre
il pericolo che futuri miglioramenti nella crittoanalisi ci costringano a cambiarli.

14.4 Una convenzione di autenticazione
Prima di proseguire introduciamo una convenzione di autenticazione. I protocolli spesso
hanno molti e diversi elementi di dati, e può risultare difficile determinare con esattezza quali elementi debbano essere autenticati. Alcuni protocolli vengono violati perché
trascurano di autenticare determinati campi. Utilizziamo una semplice convenzione per
risolvere questi problemi.
Nei nostri protocolli, ogni volta che una parte invia un’autenticazione, i dati di autenticazione consistono di tutti i dati scambiati finora: tutti i messaggi precedenti e tutti i
campi dati che precedono l’autenticazione nel messaggio dell’autenticatore. Quest’ultimo,
inoltre, sarà calcolato sulle identità delle parti che stanno comunicando. Nel protocollo
della Figura 14.1, l’autenticatore di Alice non sarebbe calcolato su k, ma sull’identificatore di Alice, sull’identificatore di Bob, X e Y. L’autenticatore di Bob sarebbe calcolato
sull’identificatore di Alice, sull’identificatore di Bob, X, Y e AuthA.
Questa convenzione chiude molte vie di attacco, e ha un costo molto ridotto. I protocolli
crittografici non scambiano così tanti dati, e i calcoli di autenticazione quasi sempre
iniziano con l’hashing della stringa di input. Le funzioni di hash sono talmente veloci
che il costo aggiuntivo è insignificante.
La convenzione inoltre ci consente di abbreviare la notazione. Anziché scrivere qualcosa
come AuthA(X, Y), scriviamo semplicemente AuthA. Poiché i dati da autenticare sono
specificati dalla convenzione, non ci serve più scriverli esplicitamente.Tutti gli altri protocolli che saranno presentati in questo libro utilizzeranno questa convenzione.
Solo una nota: le funzioni di autenticazione autenticano soltanto una stringa di byte. Ogni
stringa di byte da autenticare deve iniziare con un identificatore univoco che identifichi
il punto esatto del protocollo in cui è utilizzato l’autenticatore. Inoltre, la codifica dei
precedenti messaggi e dei campi dati in questa stringa di byte deve essere eseguita in
modo tale che sia possibile ricostruire i messaggi e i campi a partire dalla stringa senza
ulteriori informazioni di contesto. Abbiamo già parlato dettagliatamente di questo, ma
è un aspetto importante che viene spesso trascurato.

Il protocollo di negoziazione della chiave    209

14.5 Un secondo tentativo
Come risolviamo i problemi del protocollo precedente? Non vogliamo utilizzare un
insieme di parametri DH costante, perciò consentiremo ad Alice di sceglierli e inviarli a
Bob. Inoltre condenseremo i quattro messaggi in due, come mostrato nella Figura 14.2.
Alice inizia scegliendo i parametri DH e il proprio contributo DH, quindi invia tutto a
Bob con un’autenticazione. Bob deve controllare che i parametri DH siano stati scelti
opportunamente e che X sia valido (cfr. il Capitolo 11 per i dettagli di questi controlli).
Il resto del protocollo è simile alla versione precedente.Alice riceve Y e AuthB, li verifica
e calcola il risultato DH.

Alice
Sceglie (p, q, g) opportuni
x ∈ 1, . . . , q − 1

Bob
(p, q, g ), X := g x ,
AUTH A

Y := g y , AUTH B
Verifica Y, AUTH B
k ← Yx

Verifica (p, q, g ), X, AUTHA
y ∈ 1, . . . , q − 1

k ← Xy

Figura 14.2 Un secondo tentativo di negoziazione della chiave.

Non abbiamo più parametri DH fissati. Utilizziamo soltanto due messaggi, non utilizziamo direttamente la chiave di autenticazione in alcun modo, e la nostra convenzione
di autenticazione garantisce che le stringhe da autenticare non siano simili.
Ci sono però alcuni problemi nuovi.
• Che cosa facciamo se Bob vuole un numero primo DH più grande di quello di
Alice? Forse Bob utilizza norme di sicurezza più severe e ritiene che il numero primo DH scelto da Alice non sia abbastanza sicuro. Bob dovrà abortire il protocollo.
Potrebbe inviare un messaggio di errore del tipo: “Il numero primo DH deve essere
lungo almeno k bit”, ma diventerebbe troppo complicato. Alice dovrebbe riavviare
il protocollo con nuovi parametri.
• C’è un problema con l’autenticazione. Bob non è affatto sicuro che il suo interlocutore sia Alice. Chiunque potrebbe registrare il primo messaggio inviato da Alice
e poi inviarlo a Bob. Bob pensa che il messaggio provenga da Alice (dopo tutto,
l’autenticazione è stata verificata) e completa il protocollo, pensando di condividere
una chiave k con Alice. L’attaccante non determina k, poiché non conosce x, e senza
k non è in grado di violare il resto del sistema che utilizza k. Tuttavia, i log di Bob
mostreranno un protocollo autenticato con Alice, e questo è un problema di per
sé, dato che fornisce informazioni errate agli amministratori che dovessero eseguire
un’analisi.

210   Capitolo 14

Il problema di Bob si chiama mancanza di “vita”. Non è sicuro che Alice sia “viva” e
che non stia parlando con un fantasma che la impersonifica. Il metodo tradizionale per
risolvere questo problema consiste nell’assicurarsi che l’autenticatore di Alice sia calcolato
su un elemento casuale scelto da Bob.

14.6 Un terzo tentativo
Risolveremo questi problemi che alcune modifiche ulteriori. Invece di far scegliere ad
Alice i parametri DH, faremo in modo che Alice invii a Bob semplicemente i propri
requisiti minimi, e sarà Bob a scegliere i parametri. In questo modo il numero di messaggi
sale a tre (i protocolli crittografici più interessanti richiedono almeno tre messaggi. Non
sappiamo perché, ma è così). Bob invia un solo messaggio: il secondo, che conterrà il suo
autenticatore, perciò Alice dovrebbe inviare nel primo messaggio un elemento scelto a
caso. A questo scopo utilizziamo un nonce casuale.
Questo ci conduce al protocollo mostrato nella Figura 14.3. Alice inizia scegliendo s,
la dimensione minima del numero primo p che vuole usare. Sceglie anche una stringa
casuale di 256 bit come nonce Na e invia entrambi a Bob. Quest’ultimo sceglie un
insieme di parametri DH opportuno e il proprio esponente casuale, quindi invia ad
Alice i parametri, il proprio contributo DH e il proprio autenticatore. Alice completa il
protocollo DH come di consueto con l’aggiunta dell’autenticatore.

Alice

s ← min dimensione p
Na ∈ 0, . . . , 2256 − 1

Bob
s, Na

(p, q, g ), X := g x ,
Verifica (p, q, g ), X, AUTH B
y ∈ 1, . . . , q − 1

Sceglie ( p, q, g )
x ∈ 1, . . . , q − 1

Y := g y , AUTH A

k ← Xy

Verifica Y, AUTH A
k ← Yx

Figura 14.3 Un terzo tentativo di negoziazione della chiave.

C’è ancora un problema da risolvere. Il risultato finale k è un numero a dimensione
variabile; altre parti del sistema potrebbero avere difficoltà nell’utilizzarlo. Inoltre, k è
calcolato utilizzando relazioni algebriche, e la presenza di una struttura algebrica in un
sistema crittografico è sempre pericolosa. Ci sono pochi punti in cui è assolutamente
necessario che sia presente tale struttura, ma vogliamo evitarla ovunque sia possibile.

Il protocollo di negoziazione della chiave    211

Il pericolo di una struttura algebrica è che un attaccante potrebbe trovare il modo di
sfruttarla. La matematica può essere uno strumento estremamente potente. Negli ultimi
decenni abbiamo visto molte proposte di nuovi sistemi a chiave pubblica, e quasi tutte
sono state violate, per la maggior parte a causa della struttura algebrica che contenevano.
Cercate sempre di rimuovere qualunque struttura algebrica che non sia assolutamente
indispensabile.
La soluzione ovvia è quella di eseguire l’hashing della chiave finale; in questo modo la si
riduce a una dimensione fissa e si distrugge qualunque struttura algebrica rimanente.

14.7 Il protocollo finale
Il protocollo finale è mostrato in forma abbreviata nella Figura 14.4. Questa è la forma
più facile da leggere e comprendere, ma tenete conto che abbiamo lasciato fuori dal
protocollo parecchi passaggi di verifica per facilitare la lettura e per focalizzarci sulle
proprietà fondamentali. Scriviamo semplicemente “Verifica (p, q, g)” per rappresentare
diversi passaggi di verifica. Per vedere tutti i controlli crittografici richiesti, potete osservare la forma estesa del protocollo nella Figura 14.5.

Alice

s ← min dimensione p
Na ∈ 0, . . . , 2256 − 1

Bob
s, Na

(p, q, g ), X := g x ,
AUTH B
Verifica (p, q, g ), X, AUTH B
y ∈ 1, . . . , q − 1

Sceglie ( p, q, g )
x ∈ 1, . . . , q − 1

Y := g y , AUTH A

k ← SHAd -256(X y )

Verifica Y, AUTH A
k ← SHAd -256(Y x )

Figura 14.4 Il protocollo finale in forma abbreviata.

Bob deve scegliere una dimensione opportuna per p, che dipende dalla dimensione minima richiesta da Alice e da quella richiesta da Bob stesso. Naturalmente Bob dovrebbe
assicurarsi che il valore di s sia ragionevole. Non vogliamo che Bob debba mettersi a
generare numeri primi da 100.000 bit soltanto perché ha ricevuto un messaggio non
autenticato contenente un valore di s grande.Analogamente,Alice non dovrebbe mettersi
a verificare numeri primi molto grandi soltanto perché Bob li ha inviati. Quindi, Alice
e Bob limitano la dimensione di p.

212   Capitolo 14

Alice

Bob

s a ← min dimensione p
Na ∈ 0, . . . , 2256 − 1

s a , Na
s b ← min dimensione p
s ← max(s a , s b )
?
s ≤ 2 . sb
Sceglie ( p, q, g ) con log 2 p ≥ s − 1
x ∈ 1, . . . , q − 1
x
(p, q, g ), X := g ,
AUTH B

Verifica AUTH B
?
?
s a − 1 ≤ log 2 p ≤ 2 . s a
?

?

255 ≤ log 2 q ≤ 256
Verifica che p, q siano entrambi primi
?
?
?
q | (p − 1) ∧ g = 1 ∧ g q = 1
?

?

X = 1 ∧ Xq = 1
y ∈ 1, . . . , q − 1

Y := g y , AUTH A
Verifica AUTH A
?

k ← SHAd -256(X y )

?

Y = 1 ∧ Yq = 1
k ← SHAd -256(Y x )

Figura 14.5 Il protocollo finale in forma estesa.

L’uso di un massimo fissato limita la flessibilità; se i progressi della crittoanalisi dovessero improvvisamente costringervi a utilizzare numeri primi più grandi, il ricorso a un
massimo fissato costituirebbe un problema reale. L’utilizzo di un massimo configurabile
porta con sé tutti i problemi di un parametro di configurazione che praticamente nessuno comprenderebbe. Abbiamo scelto di utilizzare un massimo dinamico. Alice e Bob
rifiutano entrambi di utilizzare un numero primo di lunghezza superiore al doppio del
numero primo che preferirebbero utilizzare. Un massimo dinamico fornisce un buon
percorso di aggiornamento ed evita di utilizzare numeri primi eccessivamente grandi.
Potete ragionare sulla scelta del fattore due; forse si dovrebbe scegliere un fattore tre,
comunque non conta poi molto.
Il resto del protocollo è semplicemente un’espansione della forma breve vista in precedenza. Se Bob e Alice sono furbi, utilizzeranno entrambi memorie cache di parametri
DH adatti. Così Bob non dovrà generare ogni volta nuovi parametri DH, e Alice non
dovrà verificarli ogni volta. Le applicazioni possono anche utilizzare un insieme fisso di
parametri DH, o codificarli come valori di default in un file di configurazione, nel qual
caso non occorre inviarli esplicitamente. Un singolo identificatore di insieme di para-

Il protocollo di negoziazione della chiave    213

metri DH sarebbe sufficiente. Tuttavia, occorre prestare attenzione alle ottimizzazioni,
che possono comportare modifiche del protocollo sufficienti per comprometterlo. Non
esistono regole semplici che consentano di verificare se un’ottimizzazione comprometta
o meno un protocollo. La progettazione di protocolli è più un’arte che una scienza e
non prevede regole forti da seguire.

14.8 Visioni diverse del protocollo
Esistono vari modi per osservare un protocollo come quello qui presentato. Ci sono alcune
proprietà che il protocollo dovrebbe avere, e possiamo vedere perché le fornisce tutte.

14.8.1 La visione di Alice
Osserviamo il protocollo dal punto di vista di Alice.Alice riceve un singolo messaggio da
Bob. È certa che il messaggio proviene da Bob perché è autenticato, e l’autenticazione
include il suo nonce casuale Na. Nessuno potrebbe essere in grado di falsificare questo
messaggio, o di reinviare un messaggio vecchio.
Alice verifica che i parametri DH siano stati scelti in modo opportuno, a testimoniare
che il protocollo DH ha tutte le proprietà attese. Perciò, quando mantiene segreto y
e invia Y, sa che soltanto una persona che conosce un x tale che gx = X può calcolare
la chiave risultante k. Questa è la proprietà fondamentale del protocollo DH. Bob ha
autenticato X, e Alice confida sul fatto che Bob faccia questo soltanto quando segue il
protocollo. Quindi Bob conosce l’x appropriato, e lo mantiene segreto. Di conseguenza,
Alice è sicura del fatto che soltanto Bob conosce la chiave finale k che ricava.
Perciò Alice è convinta di parlare realmente con Bob, e cha la chiave che ricava può
essere nota soltanto a lei stessa e a Bob.

14.8.2 La visione di Bob
Ora passiamo al punto di vista di Bob. Il primo messaggio che riceve non gli fornisce
praticamente nessuna informazione utile; in sostanza afferma che qualcuno ha scelto un
valore sa e dei bit casuali Na.
Il terzo messaggio (il secondo che Bob riceve) è diverso. Questo è un messaggio che
proviene certamente da Alice, perché Alice lo ha autenticato, e abbiamo assunto inizialmente che Bob possa verificare un’autenticazione di Alice. L’autenticazione include
X, un valore casuale scelto da Bob, perciò il terzo messaggio non è una replica, ma è
stato autenticato da Alice specificamente per l’esecuzione di questo protocollo. Inoltre,
l’autenticazione di Alice copre il primo messaggio ricevuto da Bob, che perciò ora sa
che anche il primo messaggio era corretto.
Bob sa che i parametri DH sono sicuri; dopo tutto li ha scelti lui. Quindi, esattamente
come Alice, sa che soltanto chi conosca un y tale che gy = Y può calcolare la chiave finale
k. Ma Alice ha autenticato l’Y che ha inviato, e Bob si fida di Alice, perciò Alice è la sola
persona che conosce il corrispondente y. Questo convince Bob che Alice è l’unica altra
persona in grado di calcolare k.

214   Capitolo 14

14.8.3 La visione dell’attaccante
Infine osserviamo il protocollo dal punto di vista di un attaccante. Se ci siamo mettiamo
a intercettare le comunicazioni, vediamo tutti i messaggi che Alice e Bob si scambiano.
Ma la chiave k è calcolata utilizzando il protocollo DH e quindi, purché i parametri DH
siano sicuri, un attacco passivo come questo non potrà rivelare nulla su k. In altre parole,
dovremo provare un attacco attivo.
Un esercizio utile è quello di osservare ciascun elemento dati e provare a modificarlo. In
questo caso veniamo rapidamente fermati dalle due autenticazioni. L’autenticazione finale
di Alice copre tutti i dati scambiati tra Alice e Bob. Questo significa che non possiamo
modificare alcun elemento dati, possiamo soltanto provare un attacco con ripetizione di
un’esecuzione del protocollo pre-registrata. Ma il nonce e il valore casuale X bloccano
qualsiasi tentativo di ripetizione.
Ciò non significa che non possiamo provare qualcosa. Per esempio potremmo sostituire
sa con un valore più grande. Purché questo valore più grande sia accettabile per Bob, la
maggior parte del protocollo verrebbe completata normalmente. Ci sono soltanto tre
problemi. In primo luogo, aumentare il valore di sa non è un attacco perché non fa che
aumentare il numero primo DH, e quindi rafforzare i parametri DH. Il secondo e il
terzo problema sono le due autenticazioni, che falliranno entrambe.
Esistono altri casi che a prima vista potrebbero sembrare degli attacchi. Per esempio,
supponete che Alice invii a Bob sa e Na. Bob invia sa e Na a Charlie, che risponde a Bob
con (p, q, g), X e AuthC. Bob ora inoltra (p, q, g) e X ad Alice, insieme con un nuovo
autenticatore AuthB che calcola lui stesso. Alice risponde a Bob con Y e AuthA. Bob
allora invia Y e un nuovo autenticatore AuthB calcolato da lui a Charlie. Qual è il risultato di tutto ciò? Alice pensa di condividere una chiave k con Bob quando invece la
condivide con Charlie, e Charlie pensa di condividere una chiave con Bob quando invece
la condivide con Alice. Questo è un attacco? In realtà no. Notate che Bob potrebbe
semplicemente eseguire la normale negoziazione della chiave con Alice e Charlie, e poi
inoltrare tutti i messaggi sul canale sicuro (decifrando ogni messaggio che riceve da Alice
e cifrandolo nuovamente per inviarlo a Charlie, e viceversa). L’effetto è identico: Alice
pensa di comunicare con Bob e Charlie pensa di comunicare con Bob, ma in realtà stanno
inviandosi messaggi l’un l’altro. In questo scenario Bob ha più informazioni (e può fare
di più) che se avesse eseguito quella sorta di “attacco”. È vero che Alice potrebbe inviare
un messaggio a Charlie che gli faccia credere che Bob ha concordato su qualcosa, ma
questo può andare soltanto a sfavore di Bob. E un attacco che va a sfavore dell’attaccante
non è cosa di cui preoccuparsi.
Nel mondo reale troverete molti protocolli in cui vi sono elementi dati non autenticati.
La maggior parte dei progettisti non si preoccuperebbe di autenticare sa nel nostro protocollo, perché una modifica di sa non porterebbe a una possibilità di attacco (Alice e
Bob verificano entrambi, in modo indipendente, che la dimensione di p sia sufficiente).
Lasciare che gli attaccanti possano agire in qualche modo è sempre una cattiva idea. Non è
il caso di fornire loro più strumenti del necessario, e possiamo certamente immaginare una
situazione in cui la mancata autenticazione di sa potrebbe essere pericolosa. Per esempio,
supponete che Bob preferisca utilizzare parametri DH tratti da un elenco integrato nel
programma, e che ne generi di nuovi soltanto quando necessario. Finché Alice e Bob
scelgono di utilizzare dimensioni dei numeri primi DH già presenti nell’elenco, Bob
non genera mai un nuovo insieme di parametri. Ma questo significa anche che il codice

Il protocollo di negoziazione della chiave    215

di generazione dei parametri di Bob e il codice di verifica dei parametri di Alice non
vengono mai utilizzati e perciò probabilmente non saranno testati in modo appropriato.
Un bug nel codice di generazione e test dei parametri potrebbe rimanere nascosto fino
a quando un attaccante scelga di aumentare il valore sa. Naturalmente è uno scenario
improbabile, ma esistono migliaia di scenari improbabili che rappresentano un pericolo
per la sicurezza. E migliaia di rischi improbabili si sommano a formare un rischio probabile. Ecco perché siamo tanto paranoici nel bloccare qualsiasi tipo di attacco ogni volta
che possiamo. Questo ci fornisce una difesa in profondità.

14.8.4 Compromissione della chiave
Che cosa accade se un’altra parte del sistema viene compromessa? Vediamo nel seguito.
Se Alice perde la propria chiave di autenticazione senza che un attaccante arrivi a conoscerla, perde soltanto la capacità di eseguire il nostro protocollo. Può sempre utilizzare
chiavi di sessione già stabilite. Questo è sostanzialmente il comportamento atteso dal
protocollo. E lo stesso vale se è Bob a perdere la propria chiave.
Se Alice perde la chiave di sessione, senza che un attaccante arrivi a conoscerla, dovrà
eseguire nuovamente il protocollo di negoziazione della chiave con Bob per stabilire
una nuova chiave di sessione.
La situazione è peggiore se un attaccante riesce a ottenere una chiave. Se la chiave di
autenticazione di Alice è compromessa, l’attaccante può impersonificare Alice da quel
momento fino a quando Bob ne sia informato e cessi di accettare autenticazioni da Alice.
Questa è una conseguenza inevitabile. Se perdete le chiavi dell’auto, chiunque le trovi
può usarla. Del resto è una delle funzioni principali delle chiavi: consentono l’accesso a
determinate funzioni. Questo protocollo gode della proprietà desiderabile che le comunicazioni passate tra Alice e Bob continuano a rimanere segrete. Anche conoscendo la
chiave di autenticazione di Alice, l’attaccante non può trovare la chiave di sessione k per
un protocollo già completato, anche se ha registrato tutti i messaggi. A questo proposito
si parla di forward secrecy o (talvolta incontrerete l’acronimo PFS per Perfect Forward
Secrecy, ma preferiamo non utilizzare l’aggettivo “perfetta” perché non lo è mai). Le
stesse proprietà valgono per la chiave di autenticazione di Bob.
Infine, consideriamo la situazione in cui la chiave di sessione è compromessa. La chiave
k è l’hash di gxy, dove x e y sono entrambi scelti a caso. Questo non fornisce alcuna
informazione su altre chiavi, certamente non sulle chiavi di autenticazione di Alice
o di Bob. Il valore di k in un’esecuzione del protocollo è del tutto indipendente dal
valore di k in un’altra esecuzione (almeno se ipotizziamo che Alice e Bob utilizzino
un buon PRNG).
Il nostro protocollo offre la migliore protezione possibile contro le compromissioni
delle chiavi.

14.9 Complessità computazionale del protocollo
Ora esaminiamo la complessità computazionale della nostra soluzione. Assumeremo che
la scelta e la verifica del parametro DH siano tutte memorizzate in una cache, perciò non
le conteggeremo nel carico di lavoro di una singola esecuzione del protocollo.

216   Capitolo 14

Abbiamo così i calcoli seguenti, che Alice e Bob devono eseguire entrambi:
• tre elevamenti a potenza nel sottogruppo DH;
• una generazione dell’autenticazione;
• una verifica dell’autenticazione;
• varie operazioni relativamente efficienti, come generazione di numeri casuali, confronti e funzioni di hash.
Se si utilizza l’autenticazione a chiave simmetrica, il tempo di esecuzione del protocollo
è dominato dalle operazioni di elevamento a potenza DH.Vediamo di quanto lavoro si
tratta. Bob e Alice devono eseguire ciascuno tre elevamenti a potenza modulari con un
esponente di 256 bit. Ciò richiede circa 1150 moltiplicazioni modulari (con il semplice
algoritmo di elevamento a potenza binario; con un algoritmo più ottimizzato si arriva a
meno di 1000 moltiplicazioni). Per farci un’idea di quanto lavoro sia, mettiamolo a confronto con il costo computazionale di una firma RSA dove il modulo RSA e il numero
primo DH hanno la stessa dimensione. Per un modulo di s bit, l’algoritmo della firma
richiede 3s/2 moltiplicazioni se non si utilizza il teorema cinese del resto. Utilizzando
la rappresentazione CRT si ha un risparmio di un fattore quattro, perciò il costo di una
firma RSA su numeri di s bit è simile al costo di eseguire 3s/8 moltiplicazioni. Questo
ci porta a una conclusione interessante: le firme RSA sono relativamente più lente dei
calcoli DH quando i moduli sono grandi, e relativamente più veloci quando i moduli
sono piccoli. Il punto di pareggio si ha attorno a 3000 bit. Il motivo è che DH utilizza
sempre esponenti di 256 bit, mentre per RSA l’esponente aumenta all’aumentare della
dimensione del modulo.
Concludiamo che, per le dimensioni delle chiavi pubbliche da noi utilizzate, i calcoli
DH hanno un costo approssimativamente uguale a quello di un calcolo di firma RSA.
Le operazioni DH rimangono i fattori dominanti nei calcoli per il protocollo, ma il
costo è abbastanza ragionevole.
Se per l’autenticazione si utilizzano firme RSA, il carico computazionale approssimativamente raddoppia (possiamo ignorare le verifiche RSA perché sono molto veloci).
Non è ancora un costo eccessivo. La velocità delle CPU è in continuo aumento e nella
maggior parte delle implementazioni pratiche vedrete che i ritardi e i sovraccarichi
dovuti alle comunicazioni richiedono più tempo dei calcoli.

14.9.1 Trucchi di ottimizzazione
Esistono alcune ottimizzazioni applicabili alle operazioni DH. Utilizzando l’euristica
della catena di addizione, ogni operazione di elevamento a potenza può essere eseguita con un numero inferiore di moltiplicazioni. Inoltre, Alice calcola sia Xq che Xy. Si
può utilizzare l’euristica della sequenza di addizione per calcolare questi due risultati
contemporaneamente e risparmiare circa 250 moltiplicazioni. Cfr. Bos [18] per una
discussione dettagliata.
Esistono anche vari trucchi che consentono di velocizzare la generazione di un y casuale
e il calcolo di gy, ma comportano una complessità talmente elevata che preferiamo non
utilizzarli.

Il protocollo di negoziazione della chiave    217

14.10 Complessità del protocollo
Questo protocollo è anche un eccellente esempio del motivo per cui la progettazione
di protocolli è un’attività così terribilmente difficile. Anche la specifica di un protocollo
semplice come questo arriva rapidamente a riempire un’intera pagina, e non abbiamo
nemmeno incluso tutte le regole per la generazione dei parametri DH o le verifiche per
lo schema di autenticazione che non sono note al nostro livello di astrazione. E tuttavia
è già difficile seguire tutto ciò che accade. Protocolli più complessi diventano molto
più lunghi. Un particolare sistema di pagamento con smart card su cui ha lavorato Niels
Ferguson utilizzava circa una dozzina di protocolli descritti in 50 pagine di simboli e
specifiche, e il tutto utilizzando una notazione proprietaria assai compatta! Altre 50
pagine fitte servivano per trattare gli aspetti dell’implementazione critici dal punto di
vista della sicurezza.
La documentazione completa di una serie di protocolli crittografici può occupare centinaia di pagine. I protocolli diventano presto troppo complicati da tenere a mente, e
questo è pericoloso. Quando non si capisce tutto, è quasi inevitabile lasciarsi scappare
un punto debole. Il progetto citato in precedenza era probabilmente troppo complesso
per essere compreso interamente, anche dai progettisti.
Pochi anni dopo Niels Ferguson ha lavorato su un altro sistema per smart card commerciale. Era un sistema ben noto e affermato, ampiamente utilizzato per svariate applicazioni.
Un giorno Marius Schilder, un collega, si presentò con una domanda, o meglio con una
grossa falla nel sistema. Due dei protocolli presentavano un’interferenza distruttiva tra di
loro. Uno calcolava una chiave di sessione da una chiave di lungo termine sulla card, un
po’ come il protocollo di negoziazione della chiave descritto in questo capitolo; l’altro
calcolava un valore di autenticazione dalla chiave di lungo termine sulla card. Con qualche
trucco, era possibile utilizzare il secondo protocollo per fare in modo che la smart card
calcolasse la chiave di sessione, e poi inviasse la metà dei bit all’attaccante. Conoscendo
metà dei bit della chiave, era facile violare il resto del sistema. Oops! Questo bug fu
risolto nella versione successiva, ma offre un buon esempio dei problemi che possono
annidarsi nelle specifiche di protocollo molto lunghe.
I sistemi del mondo reale utilizzano sempre specifiche di protocollo molto lunghe.
Comunicare è un’attività molto complessa, e l’aggiunta di funzioni crittografiche e diffidenze complica le cose ancora di più. Il nostro consiglio è di prestare molta attenzione
alla complessità del protocollo.
Uno dei principali problemi in questo campo è che non esistono buone notazioni modulari per i protocolli, perciò si finisce per mescolare tutto insieme. Abbiamo già visto
degli esempi in questo capitolo: la negoziazione della dimensione del parametro DH,
lo scambio di chiavi DH e l’autenticazione sono tutti riuniti insieme. Non è solo una
combinazione di parti separate: specifica e implementazione si intrecciano completamente. È come un programma informatico complesso e scritto male, privo di qualsiasi
modularità. Sappiamo tutti a che cosa porta tutto ciò, ma per la complessità dei software
abbiamo sviluppato tecniche di modularizzazione. Sfortunatamente mancano tecniche
analoghe per i protocolli, e svilupparle non è un compito facile.

218   Capitolo 14

14.11 Un avvertimento
Abbiamo cercato di spiegare la progettazione di protocolli in modo che risultasse più
semplice possibile, ma non lasciatevi ingannare: è un lavoro decisamente difficile e richiede parecchia esperienza. E anche con molta esperienza, è facile sbagliare. Nonostante
tutto il nostro impegno per scrivere un libro corretto in tutti i dettagli, esiste sempre
la possibilità che il protocollo di negoziazione della chiave descritto qui sia sbagliato.
È importante assumere un atteggiamento di paranoia professionale e affrontare tutti i
protocolli con scetticismo.

14.12 Negoziazione della chiave a partire da una password
Finora abbiamo ipotizzato che esistesse un sistema di autenticazione su cui basare la
negoziazione della chiave. In molti casi non si ha a disposizione altro che una password.
Potreste utilizzare un MAC che utilizzi come chiave la password per eseguire il nostro
protocollo, ma c’è un problema: data una trascrizione del protocollo (acquisita intercettando le comunicazioni) sarebbe possibile provare qualsiasi password. Basterebbe calcolare
il valore di autenticazione e verificare se è corretto.
Il problema delle password è che gli utenti non le scelgono da un insieme molto grande.
Esistono programmi che cercano tra tutte le password più probabili. Idealmente vorremo
un protocollo di negoziazione della chiave in cui un attaccante che intercetti le comunicazioni non fosse in grado di portare un attacco di dizionario offline.
Protocolli siffatti esistono; probabilmente l’esempio più noto è SRP [129]. Questi protocolli forniscono un significativo miglioramento della sicurezza, ma non li descriviamo qui.
Se siete interessati a utilizzare un protocollo di negoziazione della chiave basato su password, tenete conto del fatto che in questo campo sono stati registrati diversi brevetti.

14.13 Esercizi
Esercizio 14.1 Nel Paragrafo 14.5 abbiamo affermato che una proprietà del protocollo
potrebbe causare la comunicazione di informazioni errate agli amministratori che analizzano il sistema. Descrivete uno scenario concreto in cui questo potrebbe rappresentare
un problema.
Esercizio 14.2 Supponete che Alice e Bob implementino il protocollo finale del
Paragrafo 14.7. Un attaccante potrebbe sfruttare una proprietà di questo protocollo per
portare un attacco di tipo denial-of-service contro Alice? E contro Bob?
Esercizio 14.3 Trovate un nuovo prodotto o sistema che utilizza (o dovrebbe utilizzare) un protocollo di negoziazione della chiave. Potrebbe essere lo stesso prodotto o
sistema che avete analizzato per l’Esercizio 1.8. Eseguite una revisione della sicurezza
di tale prodotto o sistema secondo quanto descritto nel Paragrafo 1.12, ma questa volta
focalizzandovi sugli aspetti di sicurezza e riservatezza relativi al protocollo di negoziazione
della chiave.

Capitolo 15

Aspetti relativi
all’implementazione (II)
Il protocollo di negoziazione della chiave che abbiamo progettato nel capitolo precedente introduce
nuove problematiche di implementazione.

15.1 Calcoli aritmetici su interi grandi
I calcoli che coinvolgono la chiave pubblica utilizzano
interi grandi, e come abbiamo già detto, non è facile
implementarli nel modo appropriato.
Le routine che operano su interi grandi sono quasi
sempre specifiche della piattaforma, perché così si
ottengono miglioramenti in termini di efficienza che
non si possono trascurare. Per esempio, la maggior
parte delle CPU dispone di un’operazione di addizione con riporto per implementare l’addizione di
valori di più parole. Tuttavia, in C e in quasi tutti gli
altri linguaggi di alto livello non è possibile accedere
a tale istruzione. L’esecuzione di calcoli aritmetici su
interi grandi in un linguaggio di programmazione di
alto livello è molto più lenta rispetto a un’implementazione ottimizzata per la piattaforma, e poiché questi
calcoli formano un collo di bottiglia per le prestazioni
dei sistemi a chiave pubblica, non è possibile ignorare
questo aspetto.
Non entreremo nei dettagli di come implementare
calcoli aritmetici su interi grandi, perché sono disponibili altri libri che trattano questo argomento.
Knuth [75] è una buona base di partenza, come anche
il Capitolo 14 di Handbook of Applied Cryptography
[90]. Ciò che conta veramente, dal nostro punto di
vista, è come verificare di questi calcoli.

Sommario
15.1 Calcoli aritmetici
su interi grandi
15.2 Una moltiplicazione
più rapida
15.3 Attacchi a canale
laterale
15.4 I protocolli
15.5 Esercizi

220   Capitolo 15

Nella crittografia gli obiettivi sono diversi da quelli di chi effettua l’implementazione.
Consideriamo inaccettabile un tasso di errore di 2−64 (circa uno su 18 miliardi di miliardi), mentre la maggior parte degli ingegneri sarebbe felicissimo di ottenerlo. Molti
programmatori sembrano pensare che un tasso di errore di 2−20 (circa 1 su un milione)
sia accettabile, perfino buono. Dobbiamo fare molto meglio, perché lavoriamo in un
ambiente ostile.
La maggior parte dei cifrari a blocchi e delle funzioni di hash sono relativamente facili
da testare (con due eccezioni rilevanti, IDEA e MARS, che spesso utilizzano codice
separato per questo specifico scopo). Sono pochissimi i bug di implementazione che
portano a errori difficili da individuare. Se commettete un errore nella tabella S-box di
AES, sarà individuato testando poche cifrature AES. Bastano pochi e semplici test casuali
per provare tutti i percorsi dati in un cifrario a blocchi o in una funzione di hash e trovare
rapidamente tutti i problemi a livello sistematico. Il percorso di codice intrapreso non
dipende dai dati forniti, o ne dipende in modo molto limitato. Qualsiasi serie di test
di livello accettabile per una primitiva simmetrica è in grado di provare tutti i possibili
percorsi del flusso di controllo di un’implementazione.
I calcoli aritmetici su interi grandi presentano delle peculiarità. La principale è che, nella
maggior parte delle implementazioni, il percorso del codice dipende dai dati. Raramente viene utilizzato codice che propaga l’ultimo riporto. Le routine di divisione spesso
contengono un codice che viene utilizzato soltanto una volta ogni 232 divisioni, o anche
una su 264 divisioni. Un bug in questa parte del codice non sarà rilevato da test casuali.
Il problema peggiora ulteriormente con l’utilizzo di CPU con un maggior numero di
bit. Su una CPU a 32 bit si possono eseguire 240 test casuali e aspettarsi che ogni valore
di parola a 32 bit si presenti in ogni parte del percorso dati. Questo tipo di test, però,
non funziona per le CPU a 64 bit.
La conseguenza è che occorre eseguire test estremamente accurati delle routine che
svolgono calcoli aritmetici su interi grandi. È necessario verificare che ogni percorso
di codice sia effettivamente intrapreso durante i test. A questo scopo, è necessario realizzare con grande cura i vettori di test, cosa che richiede attenzione e precisione. Non
solo occorre utilizzare ogni percorso di codice, ma occorre anche attraversare tutte le
condizioni limite. Se esiste un test con a < b, occorre verificare anche a = b − 1, a = b e
a = b + 1, naturalmente purché queste condizioni possano essere raggiunte.
Con l’ottimizzazione la situazione peggiora ulteriormente. Poiché le routine che stiamo
considerando fanno parte di un collo di bottiglia a livello delle prestazioni, il codice tende
a essere altamente ottimizzato, e ciò produce più casi particolari, più percorsi di codice
e così via. Tutti questi aspetti rendono ancora più difficile la fase di test.
Un semplice errore aritmetico può avere effetti catastrofici sulla sicurezza. Ecco un
esempio: mentre Alice sta calcolando una firma RSA, si verifica un piccolo errore
nell’elevamento a potenza p, non in quello modulo q (Alice utilizza il teorema cinese
del resto CRT per velocizzare la firma). Anziché inviare la firma corretta σ, Alice invia
σ + kq per un certo valore di k (il risultato è corretto modulo q ma errato modulo p,
perciò deve essere della forma σ + kq.) L’attaccante conosce σ3 mod n, il numero di cui
Alice sta calcolando una radice, e che dipende soltanto dal messaggio. Ma (σ + kq)3 − σ3
è un multiplo di q, e calcolando il massimo comun divisore di questo numero e di n si
ottiene q e quindi la fattorizzazione di n. Disastro!
Che fare? Per prima cosa, non implementate routine personalizzate per operazioni su
interi grandi. Utilizzate una libreria esistente. Se avete tempo a disposizione, utilizzatelo

Aspetti relativi all’implementazione (II)   221

per capire e testare la libreria scelta. In secondo luogo, sottoponete la libreria a test ben
fatti. Assicuratevi di testare ogni possibile percorso di codice. In terzo luogo, inserite test
aggiuntivi nell’applicazione. Potete scegliere tra varie tecniche.
Abbiamo discusso il problema dei test in termini di diversi percorsi di codice. Naturalmente, per evitare gli attacchi a canale laterale (cfr. i Paragrafi 8.5 e 15.3), la libreria
dovrebbe essere scritta in modo che il percorso di codice non cambi a seconda dei dati.
La maggior parte delle differenze nel percorso di codice che si verificano nei calcoli
aritmetici su interi può essere risolta mascherando le operazioni (si calcola una maschera
dalla condizione “if ” e la si utilizza per selezionare il risultato corretto). In questo modo
si risolve il problema del canale laterale, ma si ha un effetto anche sui test. Per collaudare
un calcolo “mascherato” è necessario testare entrambe le condizioni, perciò occorre generare casi di test che le raggiungano entrambe. E questo è esattamente il problema dei
test citato in precedenza. Ci siamo limitati a spiegarlo in termini di percorsi di codice,
perché pare che così risulti più facile comprenderlo.

15.1.1 Wooping
La tecnica descritta in questo paragrafo ha un nome strano: wooping. Durante un’intensa
discussione tra David Chaum e Jurjen Bos, improvvisamente sorse l’esigenza di fornire
un nome a un particolare valore di verifica. Uno dei due suggerì il nome “woop” e da
quel momento tale nome fu assegnato a tutta la tecnica di cui si discuteva. Bos in seguito
descrisse i dettagli di questa tecnica nella sua tesi di PhD [18, ch. 6], ma senza utilizzare
il nome, ritenendolo poco adatto all’ambito accademico.
L’idea alla base del wooping è quella di verificare un calcolo modulo un numero primo
piccolo scelto a caso. Pensateci come se fosse un problema di crittografia. Abbiamo una
libreria di calcoli su interi grandi che cerca di imbrogliarci fornendoci risultati errati.
Il nostro compito è quello di verificare se i risultati ottenuti sono corretti. Limitarsi a
controllare i risultati utilizzando la libreria in questione non è una buona idea, perché tale
libreria potrebbe produrre errori coerenti. Con la tecnica wooping possiamo verificare i
calcoli della libreria, purché assumiamo che non si tratti di una libreria realmente maligna,
nel senso che tenta in ogni modo di compromettere i nostri calcoli di verifica.
Per prima cosa generiamo un numero primo casuale relativamente piccolo, t, con
lunghezza dell’ordine di 64 – 128. Il valore t non deve essere fissato o prevedibile, ma
per questo abbiamo a disposizione un PRNG. Il valore t è mantenuto segreto a tutte
le parti in causa. Poi, per ogni intero grande x che si presenta nei calcoli, registriamo
anche x˜ := (x mod t). Il valore x˜ è chiamato woop di x. I valori woop hanno dimensione
fissa e generalmente sono molto più piccoli di quelli da cui sono ricavati (quindi nel
nostro caso non saranno interi grandi). Calcolare i valori woop, quindi, non comporta
un costo significativo.
Ora dobbiamo mantenere valori woop per ogni intero. Per qualsiasi input x del nostro
algoritmo, calcoliamo x˜ direttamente come x mod t. Per tutti i nostri calcoli interni,
nascondiamo i calcoli sui valori grandi nei valori woop, per calcolare il woop del risultato
senza utilizzare il risultato del calcolo sul valore intero grande.
Un’addizione normale si calcola con c := a + b. Possiamo calcolare c˜ utilizzando c˜ = a˜ +
b˜ (mod t). La moltiplicazione si tratta allo stesso modo. Potremmo verificare la correttezza
di c˜ dopo ciascuna addizione o moltiplicazione controllando che c mod t = c˜, ma è più
efficiente eseguire tutti i controlli alla fine. L’addizione modulare è appena più difficile;

222   Capitolo 15

invece di scrivere c = (a + b) mod n, scriviamo c = a + b + k · n dove k è scelto in modo
che il risultato c è compreso nell’intervallo 0, ... , n − 1. Questo è semplicemente un
altro modo di scrivere la riduzione modulare. In questo caso k è 0 o −1, assumendo che a
e b siano entrambi compresi nell’intervallo 0, ... , n − 1. La versione woop è c˜ = (a˜ + b˜ +
k˜ · n˜ ) mod t. In qualche punto all’interno della routine di addizione modulare, il valore
di k è noto. Tutto ciò che dobbiamo fare è convincere la libreria a fornirci k, in modo
che possiamo calcolare k˜.
La moltiplicazione modulare è più difficile. Anche qui dobbiamo scrivere c = a · b + k ·
n; per calcolare c˜ = a˜ · b˜ + k˜ · n˜ (mod t) ci servono a˜, b˜ , n˜ e k˜. I primi tre sono già noti,
ma k˜ dovrà essere ricavato in qualche modo dalla routine di moltiplicazione modulare.
Lo si può fare quando si crea la libreria, ma è molto difficile recuperarlo da una libreria
esistente. Un metodo generico è quello di calcolare prima a · b e poi dividere il risultato per n utilizzando una divisione con resto. Il quoziente della divisione è il valore k
che ci serve per il calcolo woop, il resto è il risultato c. Lo svantaggio di questo metodo
generico è che risulta notevolmente più lento.
Una volta che si dispone del valore woop con le moltiplicazioni modulari, è facile ottenerlo anche con gli elevamenti a potenza modulari. Le routine di elevamento a potenza
modulare costruiscono il risultato utilizzando moltiplicazioni modulari (alcune utilizzano
una routine separata di radice quadrata modulare, ma anch’essa può essere estesa con un
valore woop esattamente come quella di moltiplicazione modulare). Basta mantenere
un valore woop con ogni intero grande e fare in modo che ogni moltiplicazione calcoli
il woop del risultato attraverso i woop degli operandi.
Gli algoritmi estesi con woop calcolano il woop del risultato in base ai woop degli input:
se uno (o più) dei woop di input è errato, anche il woop di output sarà quasi certamente
errato. Perciò, se un valore woop è errato, l’errore si propaga fino al risultato finale.
Controlliamo i valori woop al termine del nostro calcolo. Se il risultato è x, tutto ciò
che occorre fare è controllare che (x mod t) = x˜. Se la libreria ha commesso degli
errori, i valori woop non corrisponderanno. Supponiamo che la libreria non generi
volutamente i propri errori in un modo che dipende dal valore t scelto. Dopo tutto,
il codice della libreria è stato rilasciato prima della nostra scelta di t, e tale codice non
è sotto il controllo dell’attaccante. È facile mostrare che qualsiasi errore che la libreria
possa fare sarà intercettato dalla stragrande maggioranza dei valori t. Perciò, aggiungendo
una verifica woop a una libreria esistente otteniamo uno strumento di test dei calcoli
estremamente valido.
L’ideale sarebbe disporre di una libreria di calcoli su interi grandi con un sistema di
verifica woop integrato. Ma non ne conosciamo alcuna.
Quando devono essere grandi i valori woop? Dipende da molti fattori. Per errori casuali,
la probabilità che il valore woop non rilevi l’errore è circa 1/t.Tuttavia, nel nostro mondo
non c’è nulla di veramente casuale. Supponiamo che nella nostra libreria vi sia un errore
software. Dobbiamo supporre che l’attaccante ne sia a conoscenza, perciò può scegliere
gli input del nostro calcolo, e non solo generare l’errore, ma anche scegliere la differenza
indotta dall’errore. Ecco perché t deve essere un numero casuale e segreto; senza conoscere
t, l’attaccante non può indirizzare l’errore nel risultato finale in modo da ottenere una
differenza che non sarebbe intercettata dal nostro metodo di wooping.
Che cosa fareste se foste un attaccante? Tentereste di far generare l’errore, naturalmente,
ma anche di fare in modo che la differenza indotta da questo sia zero modulo quanti più t
possibile. La contromisura più semplice è quella di richiedere che t sia un numero primo.

Aspetti relativi all’implementazione (II)   223

Se l’attaccante vuole fare in modo che 16 diversi numeri primi di 64 bit comportino il
mancato rilevamento dell’errore, dovrà selezionare con cura almeno 16 · 64 = 1024 bit
dell’input. Poiché per la maggior parte dei calcoli il numero dei bit di input che l’attaccante può scegliere è limitato, ciò riduce le probabilità di successo dell’attacco.
È preferibile utilizzare per t valori grandi. Esistono talmente tanti numeri primi grandi
che la probabilità di successo dell’attaccante svanisce rapidamente. Se volessimo mantenere il nostro obiettivo originale di sicurezza a 128 bit, dovremmo utilizzare un t di
128 bit, più o meno.
I valori woop non costituiscono il meccanismo di sicurezza primario del sistema, ma
soltanto un meccanismo di riserva. Se una verifica woop fallisce, sappiamo che il nostro
software presenta un bug che va corretto. Il programma dovrebbe interrompere qualsiasi
attività e indicare un errore fatale. In questo modo è molto più difficile per l’attaccante
eseguire attacchi ripetuti al sistema. Suggeriamo quindi di utilizzare per t un numero
primo casuale di 64 bit. In questo modo il carico di lavoro si riduce notevolmente, rispetto
all’utilizzo di un numero primo di 128 bit, e nella pratica il risultato è soddisfacente. Se
non potete permettervi il woop di 64 bit woop, uno di 32 bit è meglio di niente. La
maggior parte delle CPU a 32 bit è in grado di calcolare un woop di 32 bit in modo
molto efficiente, poiché dispone di istruzioni di moltiplicazione e divisione dirette.
Se incontrate un calcolo in cui l’attaccante potrebbe fornire una grande quantità di
dati, dovreste controllare anche i valori woop intermedi. La singola verifica è semplice:
?
( x mod t ) = x� . Controllando valori intermedi che dipendono solo da un limitato numero
di bit provenienti dall’attaccante, rendete più difficile il compito di imbrogliare il sistema
di woop.
Quando si utilizza una libreria di calcoli su interi grandi, è decisamente preferibile utilizzare le verifiche woop. Si tratta di un metodo relativamente semplice di evitare molti
potenziali problemi di sicurezza. Inoltre, pensiamo che sia più semplice aggiungere una
verifica woop alla libreria una volta sola, anziché aggiungere verifiche specifiche dell’applicazione per ogni impiego della libreria.

15.1.2 Verifica dei calcoli DH
Se non disponete di una libreria con verifica woop integrata, dovrete farne a meno. Il
protocollo DH, già descritto in precedenza, contiene diversi controlli, per esempio che
il risultato non sia 1 e che il suo ordine sia q; sfortunatamente, però, tali controlli non
sono eseguiti da chi fa i calcoli, ma da chi ne riceve il risultato. In generale non si devono inviare risultati errati, perché potrebbero lasciar trapelare delle informazioni, ma in
questo caso non sembrano esserci molti pericoli. Se il risultato è sbagliato, il protocollo
fallirà in un modo o nell’altro, perciò l’errore sarà notato. La sicurezza del protocollo
viene violata soltanto quando la libreria aritmetica restituisce x dove è richiesto di
calcolare gx, ma questo è un tipo di errore che un normale strumento di test ha ottime
probabilità di trovare.
Ove opportuno, probabilmente eseguiremmo DH su una libreria senza verifica woop. Gli
errori aritmetici che ci preoccupano in questa sede difficilmente porteranno a rivelare
x dal calcolo di gx. Ogni altro errore sembra innocuo, soprattutto tenendo conto che i
calcoli DH non hanno segreti di lungo termine. Continuiamo comunque a preferire
l’utilizzo di una libreria con wooping ogni volta che sia possibile, semplicemente per
sentirci più sicuri.
Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

224   Capitolo 15

15.1.3 Verifica della cifratura RSA
La cifratura RSA è più vulnerabile e richiede controlli aggiuntivi. Se qualcosa va storto,
si rischia di lasciar trapelare il segreto che si voleva cifrare, o perfino la propria chiave
segreta.
Se la verifica woop non è disponibile, esistono altri due metodi per controllare la cifratura
RSA. Supponete che l’effettiva cifratura RSA preveda il calcolo di c = m5 mod n, dove
m è il messaggio e c è il testo cifrato. Per verificarlo, potreste calcolare c1/5 mod n e confrontarlo con m. Gli svantaggi sono che si tratta di una verifica molto lenta di un calcolo
relativamente veloce, e che richiede la conoscenza della chiave privata, tipicamente non
disponibile quando si utilizza la cifratura RSA.
Un metodo probabilmente migliore è quello di scegliere un valore casuale z e verificare
che c · z5 = (m · z)5 mod n. In questo caso abbiamo tre calcoli di elevamento alla quinta
potenza: c = m5 ; il calcolo do z5 e infine il controllo che (mz)5 corrisponda a c · z5. Eventuali errori aritmetici casuali hanno alte probabilità di essere intercettati da questa verifica.
Scegliendo un valore casuale z, possiamo impedire a qualsiasi attaccante di puntare sui
valori che producono errore. Nei nostri progetti utilizziamo la cifratura RSA soltanto
per cifrare valori casuali, perciò l’attaccante non può puntare proprio su nulla.

15.1.4 Verifica delle firme RSA
Verificare le firme RSA è molto facile. Il firmatario deve semplicemente eseguire l’algoritmo di verifica della firma, che lavora in modo relativamente veloce e intercetta gli
errori aritmetici con buone probabilità. Ogni calcolo di firma RSA dovrebbe prevedere
la verifica dei risultati con il controllo della firma prodotta. Non esiste motivo valido
per saltare questo controllo.

15.1.5 Conclusioni
Vogliamo chiarire bene un punto: le verifiche di cui abbiamo parlato vanno aggiunte ai
normali test delle librerie di calcoli su interi grandi, e non si sostituiscono a questi, né
per tali librerie, né per alcun software, in particolare nel campo della sicurezza.
Se una di queste verifiche fallisce, sapete che il vostro software ha incontrato un problema. In tale situazione non c’è molto che potete fare. Continuare a lavorare non è sicuro,
perché non avete idea del tipo di errore software. L’unica cosa da fare è registrare l’errore
in un file di log e chiudere il programma.

15.2 Una moltiplicazione più rapida
Esistono molti modi per eseguire una moltiplicazione modulare più velocemente rispetto
a quello che prevede una moltiplicazione normale seguita da una divisione con resto.
Se avete la necessità di eseguire molte moltiplicazioni, il metodo di Montgomery [93]
è quello più diffuso; consultate [39] per una descrizione.
L’idea alla base del metodo di Montgomery è una tecnica per calcolare (x mod n) per
un certo x molto maggiore di n. Il metodo tradizionale della divisione con resto consiste
nel sottrarre opportuni multipli di n da x. L’idea di Montgomery è più semplice: dividere

Aspetti relativi all’implementazione (II)   225

ripetutamente x per 2. Se x è pari, lo si divide per due facendo scorrere a destra di un
bit la sua rappresentazione binaria. Se x è dispari, prima si aggiunge n (che non cambia
il valore modulo n, naturalmente) e poi si divide il risultato pari per 2 (questa tecnica
funziona soltanto se n è dispari, come avviene sempre nei nostri sistemi; esiste una generalizzazione semplice per valori pari di n). Se n è lungo k bit e x non è maggiore di
(n − 1)2, si eseguono in totale k divisioni per 2. Si otterrà un valore sempre compreso
nell’intervallo 0, ... , 2n − 1, un risultato quasi interamente ridotto modulo n.
Un momento! Abbiamo diviso per 2, perciò così otteniamo una soluzione sbagliata.
La riduzione di Montgomery in effetti non fornisce (x mod n), ma x/2k mod n per k
opportuno. L’operazione è più veloce, ma si ottiene un fattore estraneo 2−k. Esistono
varie tecniche per gestire tale fattore.
L’idea di ridefinire il protocollo per includere nei calcoli un fattore 2−k è sbagliata, perché mescola livelli diversi. Modifica la specifica del protocollo crittografico per favorire
una particolare tecnica di implementazione. Forse in futuro vorrete implementare il
protocollo su un’altra piattaforma, dove non sia opportuno utilizzare la moltiplicazione
di Montgomery (potrebbe essere una piattaforma lenta ma dotata di un potente coprocessore matematico in grado di eseguire direttamente la moltiplicazione modulare). In
quel caso, i fattori 2−k nel protocollo diventerebbero un autentico impaccio.
La tecnica standard è quella di cambiare la rappresentazione numerica. Un numero x
è rappresentato internamente da x · 2k. Se volete moltiplicare x e y, eseguite una moltiplicazione di Montgomery sulle rispettive rappresentazioni; ottenete x · 2k · y · 2k,
ma anche il fattore extra 2−k della riduzione di Montgomery, perciò il risultato finale
è x · y · 2k mod n, esattamente la rappresentazione di xy. Il costo computazionale della
riduzione di Montgomery è dato, quindi, dal costo di convertire i numeri di input nelle
rappresentazioni interne (moltiplicazione per 2k) e dal costo di riconvertire l’output nel
risultato reale (divisione per 2k). La prima conversione può essere effettuata eseguendo
una moltiplicazione di Montgomery di x e (22k mod n); la seconda può essere effettuata
eseguendo la riduzione di Montgomery per altri k bit, che divide per 2k. Non è detto
che il risultato finale di una riduzione di Montgomery sia minore di n, ma nella maggior
parte dei casi è minore di 2n − 1. In queste situazioni, un semplice test e una sottrazione
di n (opzionale) forniranno il risultato finale corretto.
Nelle implementazioni reali, la riduzione di Montgomery non è mai effettuata bit per bit,
ma per parola. Supponete che la CPU utilizzi parole di w bit. Dato un valore x, trovate un
intero piccolo z tale che la parola meno significativa di x + zn sia interamente composta
da zeri. Potete mostrare che z sarà una sola parola, e può essere calcolato moltiplicando
la parola meno significativa di x con un fattore costante costituito da una parola singola
che dipende soltanto da n. Quando la parola meno significativa di x + zn è zero, dividete
per 2w applicando al valore uno scorrimento di una parola a destra. Questa versione è
molto più veloce dell’implementazione bit per bit.

15.3 Attacchi a canale laterale
Nel Paragrafo 8.5 abbiamo discusso brevemente gli attacchi di temporizzazione e altri
attacchi a canale laterale. La brevità della trattazione non era dovuta alla scarsa pericolosità
di questi attacchi, ma al fatto che gli attacchi di temporizzazione sono utili anche contro
i calcoli a chiave pubblica, perciò riprendiamo qui l’argomento.

226   Capitolo 15

Alcuni cifrari stimolano implementazioni che utilizzano percorsi di codice diversi per
gestire situazioni speciali. Due esempi sono IDEA [84, 83] e MARS [22]. Altri cifrari
utilizzano operazioni della CPU la cui temporizzazione varia in base ai dati che elaborano.
Su alcune CPU la moltiplicazione (utilizzata da RC6 [108] e MARS) o la rotazione
dipendente dai dati (utilizzata da RC6 e RC5 [107]) hanno un tempo di esecuzione
che dipende dai dati di input. Questa caratteristica può consentire di portare attacchi
di temporizzazione. Le primitive che abbiamo utilizzato in questo libro non utilizzano
operazioni di questo tipo.AES, tuttavia, è vulnerabili ad attacchi di temporizzazione della
cache [12] o ad altri che sfruttano la differenza tra il tempo richiesto per recuperare dati
dalla memoria cache e il tempo richiesto per recuperarli dalla memoria principale.
Anche la crittografia a chiave pubblica è vulnerabile agli attacchi di temporizzazione.
Le operazioni a chiave pubblica spesso hanno un percorso di codice che dipende dai
dati, e questo porta quasi sempre a tempi di elaborazione diversi per dati diversi. I dati
di temporizzazione, a loro volta, possono favorire gli attacchi. Considerate un server
web sicuro utilizzato per il commercio elettronico. Tra le negoziazioni SSL, il server
deve decifrare un messaggio RSA scelto dal client. L’attaccante può connettersi al server,
chiedergli di decifrare un valore RSA scelto e attendere la risposta. Il tempo esatto che il
server impiega per rispondere fornisce all’attaccante informazioni importanti. Spesso si
ha che, se un certo bit della chiave è impostato a uno, gli input provenienti dall’insieme
A comportano operazioni leggermente più veloci degli input dell’insieme B, ma se il
bit è zero, non c’è differenza. L’attaccante può sfruttare questa distinzione per attaccare
il sistema: genera milioni di query da entrambi gli insiemi A e B e poi cerca di trovare
una differenza statistica nei tempi di risposta per i due gruppi. Potrebbero esserci molti
altri fattori che influenzano il tempo di risposta, ma utilizzando un numero di query
sufficientemente elevato, l’attaccante può escluderli calcolando una media. Alla fine
l’attaccante riuscirà a raccogliere dati sufficienti per misurare se i tempi di risposta per
A e B sono diversi, ottenendo così un bit di informazione sulla chiave, dopodiché può
procedere con il bit successivo.
Sembra tutto inverosimile, ma è stato fatto in laboratorio, e si potrebbe fare anche nel
mondo reale [21, 78].

15.3.1 Contromisure
Esistono diversi modi per proteggersi dagli attacchi di temporizzazione. Il più immediato consiste nell’assicurarsi che ogni calcolo richieda un tempo fisso. Tuttavia, questo
richiede l’intera libreria sia progettata tenendo presente questo obiettivo. Inoltre, esistono
fonti di differenze temporali che è quasi impossibile controllare. Alcune CPU hanno
un’istruzione di moltiplicazione che è più rapida per alcuni valori rispetto ad altri. Molte
CPU hanno schemi di cache complicati, e se lo schema di accesso alla memoria si basa
su dati non pubblici, i ritardi della cache potrebbero introdurre differenze temporali. È
quasi impossibile eliminare tutte le possibili differenze temporali, perciò ci serve un’altra
soluzione.
Un’idea ovvia è quella di aggiungere un ritardo casuale al termine di ogni calcolo, ma
questo non elimina la differenza temporale, si limita a nasconderla tra il disturbo del
ritardo. Un attaccante in grado di prendere più campioni (ovvero di fare in modo che la
macchina sotto attacco esegua più calcoli) può calcolare la media dei risultati e sperare
così di escludere il ritardo casuale aggiunto. Il numero esatto di tentativi necessari dipende

Aspetti relativi all’implementazione (II)   227

dall’ordine di grandezza della differenza temporale che l’attaccante sta cercando e del
ritardo casuale aggiunto. Negli attacchi di temporizzazione portati nel mondo reale, c’è
quasi sempre molto disturbo, quindi ogni attaccante che utilizza questo tipo di attacco
esegue comunque la media. L’unico aspetto che conta è il rapporto segnale/rumore.
Un terzo metodo è quello di rendere un’operazione tempo-costante forzando a mantenere
una durata standardizzata. Durante lo sviluppo si sceglie una durata d che sia più lunga
del tempo che potrà richiedere effettivamente il calcolo, poi si segna il tempo t in cui è
iniziato il calcolo e al termine del calcolo si inserisce un’attesa fino al tempo t + d. C’è
un leggero spreco, ma non è poi così terribile. Questa soluzione ci piace, ma protegge
soltanto da attacchi di temporizzazione puri. Se l’attaccante è in grado di ascoltare la
radiofrequenza emessa dalla macchina o di misurarne il consumo elettrico, potrebbe essere in grado di rilevare la differenza tra il calcolo effettivo e il ritardo aggiunto, e questo
consentirebbe di portare attacchi di temporizzazione e anche di altro tipo. Tuttavia, un
attacco basato sulla radiofrequenza richiede che l’attaccante sia fisicamente vicino alla
macchina e questo fatto ne riduce enormemente la pericolosità, rispetto agli attacchi
che possono essere portati via Internet.
Potete anche utilizzare tecniche derivate da firme cieche [78], che per alcuni tipi di
calcoli possono nascondere (quasi) tutte le variazioni temporali.
Non esiste una soluzione perfetta al problema degli attacchi di temporizzazione. Non è
possibile proteggere i computer disponibili sul mercato da un attacco davvero sofisticato
come quelli basati sulle radiofrequenze. Tuttavia, benché non sia possibile creare una
soluzione perfetta, se ne può realizzare una ragionevole. Basta prestare grande attenzione alla temporizzazione delle operazioni in chiave pubblica. Una soluzione ancora
migliore di quella che prevede di impostare un tempo fisso per tutte le operazioni in
chiave pubblica è quella di impostare un tempo fisso per l’intera transazione, utilizzando
la tecnica citata in precedenza. In pratica, non solo si fa in modo che le operazioni in
chiave pubblica abbiano durata prefissata, ma si fissa anche il tempo che trascorre tra
l’arrivo della richiesta e l’invio della risposta. Se la richiesta arriva al tempo t, si invia la
risposta al tempo t + C per un costante C. Tuttavia, per assicurarsi di non far trapelare
alcuna informazione di temporizzazione, occorre assicurarsi che la risposta sarà pronta
al tempo t + C, e per garantire ciò occorrerà probabilmente limitare la frequenza con
cui si accettano le richieste in arrivo, fissando un limite superiore.

15.4 I protocolli
Implementare protocolli crittografici non è diverso da implementare protocolli di comunicazione. Il metodo più immediato è quello in cui si mantiene lo stato nel program
counter e si eseguono uno a uno tutti i passaggi del protocollo. Se non si utilizza il
multithreading, l’attesa di una risposta provoca la sospensione di ogni altra attività nel
programma, e poiché non è detto che una risposta arrivi in tempi brevi, non è una
buona idea.
Una soluzione migliore è quella di mantenere uno stato esplicito del protocollo e aggiornarlo ogni volta che arriva un messaggio. Questo approccio guidato dai messaggi è
un po’ più pesante da implementare, ma fornisce maggiore flessibilità.

Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

228   Capitolo 15

15.4.1 Protocolli su un canale sicuro
La maggior parte dei protocolli crittografici viene eseguita su canali non sicuri, ma
talvolta capita di utilizzare un canale sicuro. In alcune situazioni ha senso; per esempio,
ogni utente dispone di un canale sicuro verso un centro di distribuzione delle chiavi;
quest’ultimo utilizza un protocollo semplice per distribuire le chiavi agli utenti in modo
di consentire loro di comunicare (il protocollo Kerberos è un esempio). Se eseguite un
protocollo crittografico con una controparte con cui avete già scambiato una chiave,
dovreste utilizzare la funzionalità completa del canale sicuro. In particolare, dovreste
implementare la protezione dalle repliche. È molto semplice e consente di evitare un
gran numero di attacchi al protocollo crittografico.
Talvolta il canale sicuro consente al protocollo di utilizzare delle scorciatoie. Per esempio,
se il canale sicuro fornisce la protezione dalle repliche, non è necessario che il protocollo
faccia altrettanto. La vecchia regola della modularità stabilisce comunque che il protocollo
dovrebbe ridurre al minimo la dipendenza dal canale sicuro.
Nel seguito assumeremo che il protocollo sia eseguito su un canale non sicuro. Parte
della discussione non si applica al caso del canale sicuro, ma qualche soluzione in più
non può far male.

15.4.2 Ricezione di un messaggio
Quando in uno stato del protocollo si riceve un messaggio, è necessario effettuare diversi
controlli. Il primo consiste nel verificare se il messaggio appartiene al protocollo. Ogni
messaggio dovrebbe iniziare con i campi descritti di seguito.
• Identificatore del protocollo. Identifica con esattezza il protocollo e la sua versione. Gli identificatori di versione sono importanti.
• Identificatore dell’istanza del protocollo. Identifica l’istanza del protocollo a
cui appartiene il messaggio. Alice e Bob potrebbero eseguire contemporaneamente
due protocolli di negoziazione della chiave, e tali istanze non vanno confuse.
• Identificatore del messaggio. Identifica il messaggio all’interno del protocollo.
Il metodo più semplice è quello di numerare i messaggi.
A seconda dei casi, alcuni di questi identificatori potrebbero essere impliciti. Per esempio, per protocolli eseguiti sulla propria connessione TCP, il numero di porta e il socket
associato identificano univocamente l’istanza del protocollo sulla macchina locale. I dati
dell’identificatore del protocollo e della versione devono essere scambiati soltanto una
volta. Notate però che è importante scambiarli almeno una volta per assicurarsi che
siano inclusi in qualsiasi autenticazione o firma utilizzata nel protocollo.
Dopo la verifica degli identificatori di protocollo e istanza, sappiamo a quale stato di
protocollo inviare il messaggio. Supponiamo che lo stato del protocollo abbia appena
ricevuto il messaggio n − 1 e sia in attesa di ricevere il messaggio n.
Se ricevete proprio il messaggio n, tutto va bene, basta elaborarlo secondo le regole
specificate dal protocollo. Ma se ha un numero diverso?
Se il numero del messaggio è maggiore di n o minore di n − 1, sta succedendo qualcosa
di molto strano. Un tale messaggio non dovrebbe essere stato generato, perciò deve essere
un falso costruito ad arte da qualcuno. Dovete ignorarne il contenuto.

Aspetti relativi all’implementazione (II)   229

Se ricevete il messaggio di numero n − 1, forse il messaggio di risposta che avete inviato
non è arrivato a destinazione. Questo potrebbe accadere se il protocollo è eseguito su
un sistema di trasporto inaffidabile. Poiché vogliamo ridurre al minimo le dipendenze
da altre parti del sistema, assumeremo che le cose stiano proprio così.
Per prima cosa controllate che il messaggio n − 1 appena ricevuto sia assolutamente
identico al messaggio con lo stesso numero ricevuto in precedenza. Se sono diversi,
dovete ignorare il nuovo messaggio. Inviando una seconda risposta compromettereste la
sicurezza di molti protocolli. Se i messaggi sono identici, inviate di nuovo la precedente
risposta. Naturalmente la versione che inviate ora deve essere identica a quella già inviata
precedentemente.
Se avete ignorato il messaggio ricevuto, seguendo una delle regole appena descritte, avete
una seconda decisione da prendere. Dovete interrompere l’esecuzione del protocollo? La
risposta dipende in un certo grado dall’applicazione e dalla situazione. Se il protocollo
è in esecuzione su un canale sicuro, c’è un problema molto serio. O il canale sicuro è
compromesso, oppure l’interlocutore con cui state comunicando si comporta in modo
scorretto. In entrambi i casi dovete interrompere il protocollo e chiudere il canale. Cancellate lo stato del protocollo e quello del canale, senza trascurare la chiave del canale.
Se il protocollo è in esecuzione su un canale non sicuro, uno dei messaggi che avete
ignorato potrebbe provenire da un attaccante che tenta di interferire con il protocollo.
Idealmente dovreste ignorare i messaggi dell’attaccante e completare il protocollo, ma
non è sempre possibile. Per esempio, se ricevete per primo il messaggio n − 1 falsificato
dall’attaccante, invierete una risposta. Se in seguito ricevete il “vero” messaggio n − 1,
siete costretti a ignorarlo. Non c’è rimedio da questa situazione, poiché non potete
inviare in sicurezza una seconda risposta. Tuttavia, non avete modo di sapere quale dei
due messaggi n − 1 ricevuti è quello vero, perciò, al fine di avere le migliori possibilità
di completare il protocollo con successo, dovreste registrare nel log il secondo messaggio
n − 1 come un errore e continuare normalmente. Se il messaggio a cui avete risposto
proveniva dall’attaccante, il protocollo finirà per fallire, perché i protocolli crittografici
sono appositamente progettati per impedire agli attaccanti di portarli a termine con una
delle parti che comunicano.

15.4.3 Timeout
Ogni esecuzione di un protocollo include dei timeout. Se non ottenete una risposta a
un messaggio entro un tempo ragionevole, potete reinviare il vostro ultimo messaggio.
Dopo alcuni reinvii, dovete fermarvi. Non ha senso continuare nell’esecuzione di un
protocollo quando non potete comunicare con l’altra parte.
Il modo più semplice di implementare i timeout è quello di inviare messaggi di temporizzazione allo stato del protocollo. Potete utilizzare i timer impostati esplicitamente
dal protocollo, oppure utilizzare messaggi di temporizzazione che sono inviati a periodi
regolari di pochi secondi.
Un attacco ben noto consiste nell’inviare una moltitudine di messaggi “start-of-protocol”
a una particolare macchina. Ogni volta che si riceve un messaggio start-of-protocol, si
inizializza un nuovo stato di esecuzione del protocollo. Dopo aver ricevuto milioni di
questi messaggi, la macchina esaurisce la memoria e si blocca tutto. Un buon esempio
è l’attacco SYN flood. Non esiste un metodo semplice per proteggersi da questi tipi

230   Capitolo 15

di attacchi in generale, soprattutto nell’era dei botnet e degli attacchi distribuiti, ma è
importante ricordare di eliminare i vecchi stati dei protocolli. Se un protocollo è in stallo
per troppo tempo, va eliminato.
La scelta appropriata per i tempi è oggetto di discussione. Secondo la nostra esperienza,
un pacchetto su Internet arriva entro circa un secondo, altrimenti è perso per sempre.
Inviare di nuovo un messaggio se non si è ricevuta risposta entro cinque secondi appare
ragionevole.Tre operazioni di reinvio dovrebbero bastare. Se il tasso di messaggi perduti
è così alto che si perdono quattro messaggi consecutivi su 15 secondi, la connessione non
serve a molto. Preferiamo informare l’utente di un problema dopo 20 secondi, piuttosto
che richiedergli di attendere per un minuto o due.

15.5 Esercizi
Esercizio 15.1 Considerate tutte le operazioni che un computer potrebbe eseguire
con una chiave crittografica. Quali potrebbero avere caratteristiche di temporizzazione
che potrebbero far trapelare informazioni sulla chiave?
Esercizio 15.2 Trovate un nuovo prodotto o sistema che manipoli dati segreti. Potrebbe
essere lo stesso prodotto o sistema che avete analizzato per l’Esercizio 1.8. Eseguite una
revisione della sicurezza di tale prodotto o sistema secondo le indicazioni del Paragrafo
1.12, ma questa volta concentrandovi su aspetti legati agli attacchi a canale laterale.

Parte IV

Gestione delle chiavi

In questa parte
• Capitolo 16 L’orologio
• Capitolo 17 Server di chiavi
• Capitolo 18 Le PKI dei sogni
• Capitolo 19 Le PKI nella realtà
• Capitolo 20 Considerazioni pratiche sulle PKI
• Capitolo 21 Come conservare i segreti

Capitolo 16

L’orologio

Prima di iniziare a parlare di gestione delle chiavi
(inizieremo con il capitolo seguente), dobbiamo
discutere un’altra funzione primitiva: l’orologio.
A prima vista non si tratta affatto di una primitiva
crittografica, ma poiché nei sistemi crittografici viene
spesso utilizzata l’ora corrente, ci serve un orologio
affidabile.

16.1 Impieghi di un orologio
Un orologio trova diversi impieghi in crittografia. Le
funzioni di gestione della chiave spesso sono legate a
delle scadenze. L’ora corrente può fornire un valore
univoco e un ordinamento completo di eventi. Nel
seguito esaminiamo ciascuna di queste applicazioni
in maggior dettaglio.

16.1.1 Scadenza
In molte situazioni si vuole limitare il periodo di
validità di un documento. Anche nel mondo reale
si vedono spesso periodi di validità limitati. Il metodo standard di limitare il periodo di validità di un
documento digitale è quello di inserire la data di
scadenza nel documento stesso. Ma per controllare
se un documento è scaduto, dobbiamo conoscere
l’ora corrente (completa di data). Da qui la necessità
di un orologio.

Sommario
16.1 Impieghi
di un orologio
16.2 Utilizzo del chip RTC
16.3 Pericoli per
la sicurezza
16.4 Creazione di un
orologio affidabile
16.5 Il problema dello stato
che non cambia
16.6 L’orario di riferimento
16.7 Conclusioni e consigli
16.8 Esercizi

234   Capitolo 16

16.1.2 Valore unico
Un’altra utile funzione dell’orologio, se ha una risoluzione sufficientemente elevata, è
quella di fornire un valore unico per una singola macchina. In diverse occasioni abbiamo
utilizzato dei nonce. La principale proprietà di un nonce è che non viene mai utilizzato
due volte, almeno rimanendo all’interno di un ambito ben definito. Talvolta l’ambito è
limitato, come nel caso del nonce che utilizziamo nel canale sicuro e in quello che può
essere generato mediante un contatore; in altri casi il nonce deve essere unico ogni volta
che viene riavviato il computer.
Esistono due metodi generici per generare valori nonce. Il primo è quello di utilizzare
l’ora corrente dell’orologio, con qualche meccanismo per assicurarsi di non utilizzare mai
lo stesso timecode due volte. Il secondo è quello di utilizzare un PRNG, che abbiamo
discusso nel Capitolo 9. Lo svantaggio di utilizzare un nonce casuale è che deve essere
piuttosto grande. Per raggiungere un livello di sicurezza di 128 bit si dovrebbe utilizzare
un nonce casuale di 256 bit. Non tutte le primitive supportano nonce così grandi, e
inoltre un PRNG può risultare molto difficile da implementare su alcune piattaforme,
perciò un orologio affidabile è una buona alternativa per la generazione di nonce.

16.1.3 Monotonicità
Una delle caratteristiche del tempo è che va sempre in avanti, senza mai fermarsi o tornare
indietro. Esistono protocolli crittografici che sfruttano questa proprietà. Inserendo il tempo
in un protocollo crittografico si evita che un attaccante possa tentare di inviare messaggi
vecchi fingendo che siano appartenenti al protocollo corrente, dato che il timecode dei
messaggi non rientra nell’intervallo temporale del protocollo in uso.
Un’altra importante applicazione dell’orologio si ha nelle attività di auditing e logging.
In qualsiasi tipo di sistema di transazioni è molto importante mantenere un registro degli
eventi. In caso di problemi, i log di auditing forniscono i dati necessari per tracciare la
sequenza esatta degli eventi. Inserire il tempo in ciascun evento registrato è importante,
perché senza un timestamp sarebbe molto difficile sapere quali eventi appartengono alla
stessa transazione e in quale ordine si sono verificati. Dato che gli orologi ben sincronizzati non presentano scarti significativi, il timestamp consente di mettere in correlazione
eventi di log diversi su macchine diverse.

16.1.4 Transazioni in tempo reale
Il prossimo esempio deriva dal lavoro svolto da Niels Ferguson sui sistemi di pagamento
elettronico. Per supportare pagamenti in tempo reale, la banca deve eseguire un sistema di
gestione delle transazioni finanziarie in tempo reale. Per consentire di svolgere un’attività
di auditing, deve esistere una sequenza di transazioni molto chiara. Date due transazioni A
e B, è importante sapere quale delle due è stata eseguita per prima, perché il risultato di
una di esse potrebbe dipendere dal fatto che l’altra sia stata già eseguita o meno. Il modo
più semplice per registrare tale sequenza è quello di assegnare un timestamp a ciascuna
transazione, ma funziona soltanto se si dispone di un orologio affidabile.
Un orologio inaffidabile potrebbe fornire l’ora errata. Se l’orologio per errore si sposta
all’indietro non ci sono gravi problemi: è facile verificare che l’ora corrente è maggiore
del timestamp dell’ultima transazione eseguita. Il problema è serio, invece, se l’orologio

L’orologio   235

si sposta in avanti. Supponete di aver condotto una mezzora di transazioni con l’orologio
impostato al 2020. Non potete semplicemente cambiare i timestamp di quelle transazioni,
perché non è lecito modificare manualmente registrazioni finanziarie. Non potete eseguire nuove transazioni con timestamp antecedente il 2020, perché così si scombinerebbe
l’ordine delle transazioni, che è determinato dal timestamp. Esistono modi per risolvere
questo problema, ma è certamente preferibile utilizzare un orologio affidabile.

16.2 Utilizzo del chip RTC
La maggior parte dei computer desktop contiene un chip RTC (Real-Time Clock) e
una piccola batteria. Si tratta in effetti di un piccolo orologio digitale integrato nella
macchina, grazie al quale il computer sa che ora è quando lo avviate al mattino. Perché
non utilizzare questo orologio?
Il chip RTC va bene per l’utilizzo normale, ma in un sistema di sicurezza è necessario
imporre standard più elevati. All’interno di un sistema di sicurezza, l’orologio dovrebbe
fornire l’ora corretta anche nel caso in cui un attaccante tentasse di manipolarlo. Inoltre,
per impieghi normali, un orologio che mostri l’ora errata è fastidioso ma non pericoloso,
mentre in un sistema di sicurezza gli errori dell’orologio possono provocare seri danni.
Gli orologi RTC tipicamente integrati nell’hardware in commercio non sono affidabili
e sicuri quanto servirebbe per un sistema di sicurezza. Abbiamo personalmente sperimentato vari guasti dei chip RTC nell’ultimo decennio. Inoltre questi guasti si sono
verificati spontaneamente, senza l’intervento di un attaccante che tentasse di danneggiare
l’orologio. Nella maggior parte dei casi si tratta di guasti molto semplici: su una vecchia
macchina, la batteria si esaurisce e l’orologio si ferma o si riporta al 1980; oppure, un
giorno si avvia la macchina e l’orologio è impostato al 2028. Talvolta un orologio resta
indietro o va avanti rispetto all’ora corrente.
A parte gli errori accidentali degli orologi RTC, occorre considerare l’eventualità di
attacchi attivi. Qualcuno potrebbe tentare di manipolare l’orologio in qualche modo. A
seconda del tipo di computer e di sistema, cambiare l’impostazione dell’orologio può
essere più o meno facile. Su alcuni sistemi serve l’accesso speciale dell’amministratore per
impostare l’orologio, mentre in altri l’impostazione può essere effettuata da chiunque.

16.3 Pericoli per la sicurezza
Esistono diversi tipi di attacchi che possono essere portati contro un sistema che utilizza
un orologio.

16.3.1 Portare indietro l’orologio
Supponete che l’attaccante sia in grado di regolare l’orologio su un tempo arbitrario del
passato. In questo modo potrebbe causare ogni tipo di guai. La macchina crede erroneamente di vivere nel passato. Forse un attaccante in passato aveva accesso ad alcuni dati
perché era impiegato presso un’azienda, ma poi è stato licenziato e non ha più l’accesso; se
l’orologio è impostato sull’ora sbagliata, il computer potrebbe consentire ancora a questo
ex dipendente di accedere a dati riservati. Questo problema potrebbe presentarsi ogni

236   Capitolo 16

volta che si revoca l’accesso a un utente, perché riportando indietro l’orologio sarebbe
possibile ripristinare l’accesso (naturalmente dipende da come sono state progettate le
altre parti del sistema).
Un’altra interessante via di attacco è rappresentata dalle attività automatizzate. Supponete che un computer dell’ufficio del personale effettui automaticamente i pagamenti
degli stipendi alla fine del mese, mediante versamenti diretti sui conti bancari. Attività
automatizzate come queste sono avviate da un programma che controlla data e ora e
segue un elenco di operazioni da compiere. Portando indietro ripetutamente l’orologio
si può fare in modo che le attività siano ripetute più volte. Se un’attività è impostata per
l’avvio a mezzanotte, l’attaccante regola l’orologio sulle 23.55 e attende l’avvio. Quando
l’attività è terminata, l’attaccante riporta nuovamente indietro l’orologio; teoricamente
potrebbe continuare fino all’esaurimento dei fondi aziendali.
Un altro problema si verifica nei sistemi finanziari. È importante determinare data e ora
corretta di una transazione, perché i calcoli degli interessi dipendono da quando una
transazione è stata eseguita. Se utilizzate una carta di credito con rimborso rateale, sarebbe
vantaggioso convincere il computer della vostra banca che un versamento online che
avete appena fatto è avvenuto sei mesi fa, evitando così di pagare sei mesi di interessi.

16.3.2 Fermare l’orologio
Tutti i progettisti sanno per istinto che il tempo non si ferma mai. È un presupposto
fin troppo ovvio per parlarne o documentarlo. I sistemi progettati fanno affidamento
sul normale scorrere del tempo. Tuttavia, se l’orologio viene fermato, il tempo appare
sospeso, e molti sistemi potrebbero comportarsi in modi imprevedibili.
Tra i problemi più semplici vi sono l’inserimento di timestamp errati in log di auditing
e report. Il tempo esatto di una transazione può avere importanti conseguenze dal punto
di vista finanziario, e l’invio di una relazione con la data sbagliata potrebbe portare a
effetti sgraditi.
Altri problemi potrebbero verificarsi con la visualizzazione dell’ora. Supponete che il
programmatore dell’interfaccia utente abbia utilizzato un semplice sistema per mostrare
la situazione corrente a un broker che lavora in tempo reale. Ogni dieci secondi lo schermo viene aggiornato con gli ultimi dati. Ma non tutti i dati delle transazioni finanziarie
arrivano alla stessa velocità, a causa di ritardi di vario tipo. Limitandosi a riportare gli
ultimi dati ricevuti si rischierebbe di fornire una visione non corretta della situazione
finanziaria. Forse una parte di una transazione è stata già riportata e l’altra no. Una somma di denaro potrebbe essere visualizzata sul conto bancario prima della vendita di un
pacchetto azionario. I contabili non amano ottenere report in cui i conti non tornano.
Perciò il programmatore fa qualcosa di meglio. Tutti i report di transazioni finanziarie
sono contrassegnati con un timestamp e registrati in un database locale. Per visualizzare
un report coerente, si considera un particolare istante temporale e si indica la situazione
finanziaria in tale istante. Per esempio, se il sistema più lento ha un ritardo di cinque
secondi nella generazione del report, si sceglie di visualizzare la situazione finanziaria di
sette secondi prima. Così si aumenta un po’ il ritardo della visualizzazione, ma si garantisce
un report corretto. O almeno, è così finché l’orologio viene fermato: improvvisamente
il programma riporta sempre la stessa situazione: quella di sette secondi prima rispetto
al tempo (sbagliato) indicato dall’orologio. Oops!

L’orologio   237

16.3.3 Portare avanti l’orologio
Portando avanti l’orologio si induce il computer a lavorare come se fosse nel futuro.
Questo apre la strada a semplici attacchi del tipo DoS (Denial-of-Service). Se l’orologio
è impostato in avanti di quattro anni, tutte le transazioni delle carta di credito vengono
rifiutate perché tutte le carte risultano scadute. Non si possono nemmeno prenotare
biglietti aerei online, perché non ci sono ancora gli orari per le date considerate.
Le offerte più significative nelle aste di eBay sono inviate negli ultimi secondi prima della
scadenza. Se aveste la possibilità di spostare in avanti l’orologio di eBay solo di poco, potreste tagliar fuori molte offerte dei concorrenti e vincere l’asta a un prezzo inferiore.
Un nostro amico sperimentò un problema di questo tipo con il proprio sistema di fatturazione. A causa di un errore software, l’orologio fece un salto in avanti di 30 anni. Il
sistema di fatturazione iniziò a sollecitare tutti i clienti per 30 anni di fatture non pagate.
In questo caso non vi furono perdite finanziarie dirette, ma le cose sarebbero state diverse
se si fossero utilizzati sistemi di addebito diretto su conti o carte di credito. Certamente
non fu un bene per le relazioni con i clienti.
L’impostazione dell’orologio su un tempo futuro presenta anche pericoli diretti. Esistono molte situazioni in cui certi dati devono essere mantenuti segreti fino a un tempo
specifico, e resi pubblici dopo tale scadenza. In un sistema automatizzato, l’impostazione
in avanti dell’orologio consentirebbe di accedere ai dati. Nel caso di un profit warning
per una società quotata sul mercato azionario, per esempio, l’accesso prematuro alle
informazioni potrebbe consentire di ottenere un illecito profitto.

16.4 Creazione di un orologio affidabile
Non disponiamo di una soluzione semplice per il problema dell’orologio. Possiamo
suggerire alcune idee e alcune tecniche, ma poiché i dettagli dipendono dall’ambiente
di lavoro e dall’analisi dei rischi, non possiamo fornire soluzioni universali. Il nostro
obiettivo, quindi, si rivolge in più direzioni: migliorare la comprensione di come utilizzare l’orologio, incoraggiare a ridurne al minimo la dipendenza, individuare gli aspetti
fondamentali da prendere in considerazione e fornire un esempio di come ragionare
per costruire un orologio affidabile.
La maggior parte dei computer ha, o può implementare, qualche tipo di contatore che
si avvia quando si accende il computer. Potrebbe essere un contatore del numero di cicli
della CPU, un contatore di refresh o qualcosa di simile. Tale contatore può essere usato
per tenere traccia del tempo trascorso dall’ultimo riavvio. Non è un orologio, perché
non fornisce informazioni sull’ora effettiva, ma può essere utilizzato per misurare il
tempo trascorso tra due eventi, purché questi si siano verificati entrambi dopo l’ultimo
riavvio.
Questo tipo di contatore, quantomeno in relazione al nostro problema dell’orologio,
serve principalmente per controllare l’eventualità di errori accidentali nell’orologio RTC.
Se l’orologio di sistema non funziona correttamente, mostrerà delle discrepanze rispetto
al contatore. La verifica è semplice e fornisce indicazioni per alcune modalità di errore
del chip RTC. Notate che la corrispondenza tra tempo dell’orologio RTC e valore del
contatore deve essere modificata se l’impostazione dell’orologio viene modificata da un
utente autorizzato.

238   Capitolo 16

Un altro controllo semplice consiste nel tenere traccia dell’ultimo spegnimento del sistema,
o dell’ultima volta che sono stati scritti dei dati su disco. L’orologio non dovrebbe mai
andare all’indietro. Se la macchina improvvisamente si avvia nell’anno 1980, è evidente
che c’è qualcosa che non va. È anche possibile fare in modo che l’orologio non salti
troppo in avanti. La maggior parte dei computer viene avviata almeno una volta alla
settimana; potrebbe valere la pena di chiedere all’utente di confermare l’esattezza della
data se la macchina non è stata avviata da un tempo maggiore (poiché la maggior parte
degli utenti farà clic sul pulsante OK senza nemmeno leggere il messaggio, probabilmente è meglio chiedere di inserire la data corretta, senza visualizzare quella indicata
dall’orologio di sistema). In questo modo si intercetterebbe il caso di un orologio di
sistema che salta in avanti di più di una settimana. Naturalmente stiamo ipotizzando che
l’utente non sia l’avversario.
Esistono altri metodi per controllare l’ora. Si può utilizzare un time server su Internet o su
una intranet. Esistono protocolli di sincronizzazione dell’ora ampiamente diffusi, come
NTP [92] o SNTP [91]. Alcuni forniscono anche l’autenticazione dei dati temporali, in
modo che un attaccante non possa ingannare la macchina. Naturalmente l’autenticazione
richiede un’infrastruttura per la gestione delle chiavi. La chiave condivisa con il time
server potrebbe essere una chiave simmetrica configurata manualmente, ma sarebbe un
procedura noiosa. Si potrebbe anche utilizzare una PKI, ma come vedremo nel Capitolo 18, la maggior parte dei sistemi PKI necessita di un orologio, per cui ricadiamo nel
dilemma dell’uovo e la gallina. Se vi affidate alla protezione crittografica offerta da un
protocollo di sincronizzazione dell’orologio, prestate molta attenzione, perché la sicurezza
dell’intero sistema potrebbe dipendere da tale protocollo.

16.5 Il problema dello stato che non cambia
Arriviamo così a esaminare un serio problema che può presentarsi su alcune piattaforme
hardware. Si tratta dei piccoli computer embedded, come quelli inseriti nelle serrature
elettroniche o nei lettori di smart card, e costituiti generalmente da una piccola CPU,
una piccola quantità di memoria RAM, memoria non volatile (per esempio flash) per
memorizzare il programma, alcuni canali di comunicazione e altro hardware specifico
dell’impiego.
Notate che spesso non è incluso un orologio RTC. L’aggiunta di un orologio in tempo
reale richiede un chip in più, un cristallo oscillatore e, cosa più importante, una batteria.
A parte il costo aggiuntivo, l’inserimento di una batteria complica il dispositivo, poiché
richiede di controllare se si esaurisce, può comportare problemi legati a variazioni della
temperatura, e i composti chimici tossici di alcune batterie possono anche comportare
problemi di trasporto dell’hardware. Per tutti questi motivi, molti piccoli computer non
dispongono di un orologio RTC.
Ogni volta che uno di questi piccoli computer si avvia, parte esattamente nello stesso
stato. Legge lo stesso programma dalla stessa memoria non volatile, inizializza l’hardware
e avvia le operazioni. Poiché questo libro tratta di crittografia, assumeremo che nella
comunicazione con altre parti del sistema sia utilizzato qualche tipo di protocollo crittografico. Ma ecco il problema: senza un orologio o un generatore di numeri casuali di tipo
hardware, il sistema embedded ripeterà sempre lo stesso comportamento. Supponete che
un attaccante attenda finché un computer integrato in una serratura abbia la necessità di

L’orologio   239

aprire un portone di ingresso per far passare un camion; l’attaccante riavvia il computer
della serratura appena prima del momento dell’apertura (per esempio interrompendo
momentaneamente l’alimentazione elettrica). Dopo alcune procedure di inizializzazione,
il sistema centrale ordinerà al computer della serratura di aprire il portone, inviandogli
dei messaggi tramite il canale di comunicazione. Il giorno seguente l’attaccante riavvia di
nuovo il computer della serratura e invia esattamente gli stessi messaggi della prima volta.
Poiché il computer si avvia nello stesso stato e vede gli stessi input, si comporta allo stesso
modo e apre il portone. Male. Notate che non importa se il computer della serratura
utilizza un protocollo di sincronizzazione dell’orario: i messaggi del protocollo possono
essere replicati utilizzando quelli del giorno precedente e il computer non ha modo di
accorgersene. Il problema dello stato che non cambia non è risolto da alcun protocollo.
Un chip RTC risolve questo problema. Il piccolo computer embedded può cifrare
l’orario corrente con una chiave segreta fissata per generare dati altamente casuali, che
a loro volta possono essere utilizzati come nonce in un protocollo crittografico. Poiché
l’orologio RTC non ripete mai il proprio stato, il computer embedded evita la trappola
dello stato che non cambia.
Un generatore di numeri casuali hardware ha lo stesso effetto: consente al computer
embedded di comportarsi in modo diverso ogni volta che viene riavviato.
Tuttavia, se non è disponibile un orologio RTC o un generatore di numeri casuali, il
problema è serio.Talvolta è possibile tentare di ricavare una casualità dal disallineamento tra
l’oscillatore dell’orologio locale e quello del temporizzatore di rete o un altro oscillatore, ma
è molto difficile ricavare una quantità di entropia sufficiente in un tempo breve. Spendere
10 minuti per riavviare un computer embedded è semplicemente inaccettabile.
Abbiamo incontrato più volte il problema dello stato che non cambia. La conclusione è
che serve una modifica hardware per poter utilizzare la crittografia su piccoli computer
del tipo interessato. È difficile convincere di questo i manager, soprattutto perché spesso
l’hardware è già posto in opera e nessuno vuole sentirsi dire che non c’è nulla da fare.
Ma non esiste una formula magica che consenta di rendere sicuro un sistema non sicuro.
Se non si tiene conto della sicurezza fin dall’inizio della progettazione di un sistema, non
si ottiene quasi mai un risultato di buon livello.
Esiste un’altra soluzione possibile, anche se raramente funziona nella pratica. A volte è
possibile mantenere un contatore dei riavvii nella memoria non volatile. Ogni volta che la
CPU si riavvia, incrementa un contatore mantenuto nella memoria non volatile. Questa
soluzione presenta parecchi problemi. Alcune memorie non volatili possono essere aggiornate soltanto poche migliaia di volte, perciò il continuo aggiornamento del contatore
causa l’usura della macchina. Alcune tecnologie di memoria non volatile richiedono un
voltaggio elettrico aggiuntivo per poter essere programmate, e spesso non esiste la possibilità di applicarlo in opera. In alcuni progetti la memoria non volatile consente soltanto
di impostare dei bit, oppure di effettuare un azzeramento totale. La seconda opzione non
è praticabile, perché si perderebbe anche il programma principale della macchina. Anche
superando tutti questi problemi, rimane molto difficile modificare la memoria non volatile
in modo che il contatore sia incrementato in modo affidabile anche se l’alimentazione
della macchina può essere interrotta in momenti del tutto arbitrari. Questa soluzione
del contatore nella memoria non volatile è praticabile soltanto in una minoranza dei casi
che abbiamo incontrato. Quando possibile, un contatore simile potrebbe essere utilizzato
all’interno di un PRNG. Per esempio, il contatore potrebbe essere utilizzato con la modalità CTR e una chiave AES per generare un flusso di bit pseudocasuali.

240   Capitolo 16

16.6 L’orario di riferimento
Nella discussione degli orologi, è utile qualche riflessione su quale orario di riferimento
scegliere. L’ora locale, quella riportata sugli orologi tradizionali, non va bene, perché cambia
a seconda del fuso orario e dell’ora legale. Questi cambiamenti pongono dei problemi:
alcuni valori temporali si ripetono ogni anno quando gli orologi sono riportati indietro
di un’ora in autunno, quindi il tempo non è più unico o monotonico. Alcuni valori
temporali sono impossibili quando gli orologi sono portati avanti di un’ora in primavera.
Inoltre, la data esatta in cui scatta l’ora legale cambia a seconda del paese. In alcuni paesi
le regole cambiano dopo pochi anni, e si dovrebbe aggiornare il software per tenerne
conto. Infine, chi viaggia con un computer portatile potrebbe regolare impostare l’ora
adattandola al fuso orario locale, facendo peggiorare ulteriormente la situazione.
La scelta più ovvia è quella di utilizzare l’ora UTC. Si tratta di uno standard internazionale per la misura del tempo, basato su orologi atomici e ampiamente diffuso in tutto il
mondo. Qualunque computer può tenere traccia della distanza tra ora locale e ora UTC
e utilizzare tale informazione nelle interazioni con l’utente.
L’ora UTC presenta un problema: il secondo intercalare. Per mantenere sincronizzata
l’ora UTC con la rotazione terrestre, si applica una correzione di un secondo a periodi
specifici (di qualche anno più o meno). Finora si sono sempre aggiunti secondi in più;
esiste un minuto particolare che è durato 61 secondi. Teoricamente sarebbe possibile
avere anche un secondo mancante. Dipende tutto dalla rotazione terrestre. Il problema
per i computer è che i secondi intercalari sono impossibili da prevedere in anticipo.
Ignorandoli si generano delle imprecisioni nella misurazione di intervalli temporali.
Non è proprio un problema di crittografia, ma se si vuole realizzare un buon orologio,
occorre tenerne conto. Tutti i software presuppongono che ogni minuto abbia 60 secondi. Se si effettua la sincronizzazione diretta con orologio UTC reale, l’inserimento
di un secondo intercalare comporta dei problemi: probabilmente l’orologio interno si
ripeterà per un secondo. È un problema minore, ma distrugge le proprietà di unicità e
monotonicità dei valori temporali.
Per la maggior parte delle applicazioni, la sincronizzazione esatta dell’orologio è meno
importante della monotonicità e dell’unicità dei timestamp. Purché ci si assicuri che
l’orologio non faccia mai un salto all’indietro in corrispondenza di un secondo intercalare,
non importa in che modo si risolve questo problema.

16.7 Conclusioni e consigli
Sfortunatamente non abbiamo una soluzione ideale da proporre. Creare un orologio
affidabile è molto complicato, soprattutto in un ambiente crittografico in cui si assume
che esistano attaccanti malintenzionati. La soluzione migliore dipende dalla situazione
locale. Consigliamo di tenere presente che l’uso di un orologio può comportare potenziali
problemi di sicurezza, di ridurre al minimo la dipendenza dall’orologio e di essere cauti.
E, di nuovo, l’aspetto più importante è che siano assicurate la monotonicità e l’unicità
dei timestamp.

L’orologio   241

16.8 Esercizi
Esercizio 16.1 Alcuni computer utilizzano il protocollo NTP all’avvio o a intervalli
regolari. Disattivate il protocollo NTP per una settimana sul vostro computer. Scrivete
un programma che a intervalli regolari (almeno una volta ogni due ore) registri sia
l’orario reale, sia quello riportato dal computer. Sia t0 il tempo iniziale reale all’inizio
dell’esperimento. Per ogni coppia di misurazioni, tracciate l’orario reale meno t0 sull’asse
orizzontale di un grafico e l’orario del computer meno quello reale sull’asse verticale. Che
differenza c’è tra l’orologio del vostro computer e il tempo reale, dopo una settimana?
Il grafico indica altro?
Esercizio 16.2 Ripetete l’Esercizio 16.1, ma questa volta per un gruppo di cinque
diversi computer.
Esercizio 16.3 Trovate un nuovo prodotto o sistema che utilizzi (o dovrebbe utilizzare) un orologio. Potrebbe essere lo stesso prodotto o sistema che avete analizzato per
l’Esercizio 1.8. Effettuate una revisione della sicurezza di tale prodotto o sistema secondo
le indicazioni del Paragrafo 1.12, ma questa volta concentrandovi su aspetti di sicurezza
e riservatezza legati all’orologio.

Capitolo 17

Server di chiavi

Siamo finalmente giunti al tema della gestione delle
chiavi, senza dubbio tra i più difficili nei sistemi crittografici.Abbiamo discusso come cifrare e autenticare
dati, e come negoziare una chiave segreta condivisa
tra due parti. Ora dobbiamo trovare un modo affinché Alice e Bob possano riconoscersi a vicenda su
Internet. Come vedrete, la situazione diventa ben
presto molto complessa. La gestione delle chiavi è
particolarmente difficile perché coinvolge le persone,
molto più difficili da capire e prevedere rispetto agli
elementi matematici. Questo argomento per molti
versi riassume tutto quanto abbiamo discusso finora.
Gran parte dei vantaggi della crittografia va perduta
se la gestione delle chiavi non è svolta bene.
Prima di iniziare, chiariamo un punto. Parleremo
soltanto degli aspetti crittografici della gestione delle
chiavi, non di quelli organizzativi come le norme che
indicano a chi assegnare le chiavi, quali chiavi fanno
accedere a quali risorse, come verificare l’identità
delle persone che ricevono delle chiavi, i criteri per
la sicurezza delle chiavi archiviate, i meccanismi per
verificare che tali criteri siano rispettati e così via.
Ogni organizzazione implementerà questi aspetti
in modo diverso in base alle proprie esigenze e alla
propria infrastruttura organizzativa. Noi ci concentreremo soltanto sulle parti che riguardano direttamente
la crittografia.
Un modo per attuare la gestione delle chiavi consiste
nell’affidare a un’entità fidata l’assegnazione di tutte le
chiavi. Questa entità è il server di chiavi, o key server.

Sommario
17.1 Concetti di base
17.2 Kerberos
17.3 Soluzioni più semplici
17.4 Che cosa scegliere
17.5 Esercizi

244   Capitolo 17

17.1 Concetti di base
L’idea di base è semplice: supponiamo che chiunque imposti una chiave segreta condivisa
con il server di chiavi. Per esempio, Alice imposta una chiave KA che è nota soltanto a
lei stessa e al server di chiavi. Bob imposta una chiave KB che è nota soltanto a lui stesso
e al server di chiavi. Le altre parti impostano le chiavi allo stesso modo.
Ora supponiamo che Alice voglia comunicare con Bob. Non dispone di una chiave che
può utilizzare a questo scopo, ma può comunicare in modo sicuro con il server di chiavi,
che a sua volta può comunicare in modo sicuro con Bob. Potremmo semplicemente
inviare tutto il traffico al server di chiavi e farlo operare come una sorta di ufficio postale
gigante, ma in questo modo il server dovrebbe gestire quantità di traffico enormi. Una
soluzione migliore consiste nel chiedere al server di chiavi di impostare una chiave KAB
condivisa da Alice e Bob.

17.2 Kerberos
Questa è l’idea centrale di Kerberos, un sistema di gestione delle chiavi molto diffuso
[79], che si basa sul protocollo Needham-Schroeder [102].
Il meccanismo, a livello di base, è il seguente. Quando Alice vuole comunicare con Bob,
prima contatta il server di chiavi. Questo invia ad Alice una nuova chiave segreta KAB
più la chiave KAB cifrata con la chiave di Bob KB. Entrambi questi messaggi sono cifrati
con KA, perciò soltanto Alice può leggerli.Alice invia il messaggio cifrato con la chiave di
Bob, chiamato ticket, a Bob. Bob lo decifra e ottiene KAB, che ora è una chiave di sessione
nota soltanto ad Alice e Bob (e naturalmente anche al server di chiavi).
Una delle caratteristiche di Kerberos è che il server di chiavi, denominato KDC nella
terminologia di Kerberos, non deve aggiornare molto spesso il proprio stato. Naturalmente il server di chiavi deve ricordare la chiave che condivide con ciascun utente, ma
quando Alice chiede al KDC di impostare una chiave tra lei e Bob, il server esegue il
compito e poi se ne dimentica del tutto. Non tiene traccia di quali chiavi ha impostato
tra gli utenti. È una buona scelta, perché consente di distribuire un server di chiavi molto
occupato su diverse macchine in modo semplice. Poiché non c’è uno stato da aggiornare,
Alice può comunicare con una copia del server di chiavi in un certo momento e con
un’altra copia in un altro momento.
I protocolli crittografici richiesti per un sistema tipo Kerberos sono molto complessi.
Inizialmente progettare tali protocolli sembra abbastanza semplice, ma in realtà anche
crittografi molto esperti hanno pubblicato delle proposte che in seguito sono state violate. I difetti sono molto insidiosi. Non esamineremo questi protocolli, troppo pericolosi
per fare esperimenti e modificarli manualmente. E non ci viene nemmeno in mente di
metterci a progettare protocolli di questo tipo partendo da zero. Se vi serve un protocollo
come questi, utilizzate l’ultima versione di Kerberos, che è ormai noto da parecchio
tempo ed è stato esaminato da molte persone competenti.

Server di chiavi   245

17.3 Soluzioni più semplici
A volte non è possibile usare Kerberos. Il protocollo non è affatto semplice e impone
alcuni vincoli. I server devono memorizzare tutti i ticket che hanno accettato, e ogni utente
necessita di un orologio affidabile. Esistono diverse situazioni in cui è impossibile soddisfare
questi requisiti. Inoltre, riteniamo più istruttivo studiare un progetto più semplice.
Possiamo creare una soluzione più semplice e più robusta se non diamo un’eccessiva importanza all’efficienza. È particolarmente utile consentire al server di chiavi di mantenere
i dati dello stato. I computer moderni sono molto più potenti di quelli utilizzati ai tempi
in cui fu progettato Kerberos, e non dovrebbero avere problemi a mantenere lo stato per
migliaia di utenti. Anche un sistema molto grande con 100.000 utenti non costituisce
un problema: se ognuno di essi richiede 1 kilobyte per la memorizzazione dello stato sul
server di chiavi, per memorizzare tutti gli stati basterebbero 100 megabyte di memoria.
Il server deve comunque essere abbastanza veloce per impostare tutte le chiavi richieste,
ma anche questo non è un gran problema con i computer moderni.
Esamineremo soltanto la situazione in cui c’è un unico server di chiavi.
Esistono tecniche utilizzabili per distribuire lo stato del server di chiavi su diversi computer, ma non entriamo nei dettagli, perché in realtà un server di chiavi per decine o
migliaia di utenti è troppo rischioso. Il rischio dei server di chiavi molto grandi è che
tutte le chiavi risiedono in un unico luogo, che diventa un obiettivo di attacco molto
attraente. Inoltre, il server di chiavi deve essere sempre online, perciò un attaccante può
sempre comunicare con esso. Le migliori tecniche disponibili oggi non proteggono molto
bene i computer dagli attacchi di rete, e mantenere tutte le chiavi in un’unica posizione
significa porre le basi per una catastrofe. Per sistemi più piccoli il “valore” complessivo
delle chiavi mantenute dal server di chiavi è più ridotto, e così anche le minacce sono
meno forti (non amiamo lasciare che il sistema sia sottoposto a minacce prive di efficaci
contromisure, ma quando si tratta di gestione delle chiavi, si finisce sempre per arrivare
a una soluzione di compromesso). Nei capitoli seguenti esamineremo una soluzione più
adatta a sistemi molto grandi. Ora limiteremo la nostra discussione ai server di chiavi per
sistemi abbastanza piccoli, fino a poche migliaia di utenti.

17.3.1 Connessione sicura
Descriviamo brevemente una soluzione più semplice. Innanzitutto assumiamo che
Alice e il server di chiavi condividano una chiave KA. Anziché utilizzare questa chiave
direttamente, la usano per eseguire un protocollo di negoziazione della chiave, come
quelli discussi nel Capitolo 14 (se KA è una password, sarebbe meglio utilizzare uno dei
protocolli adatti per password a bassa entropia discussi nel Paragrafo 14.12, supponendo
che non vi siano problemi legati ai brevetti). Il protocollo di negoziazione della chiave
imposta una nuova chiave K'A tra il server di chiavi e Alice. Tutti gli altri partecipanti
eseguono o stesso protocollo con il server di chiavi, e tutti ottengono chiavi nuove.
Alice e il server di chiavi utilizzano K'A per creare un canale di comunicazione sicuro
(cfr. il Capitolo 7 per i dettagli), con cui possono comunicare in sicurezza. Il canale sicuro
fornisce riservatezza, autenticazione e protezione dalle repliche. Tutte le comunicazioni
successive si svolgono sul canale sicuro. Tutti gli altri partecipanti creano un analogo
canale sicuro con il server di chiavi.

246   Capitolo 17

17.3.2 Impostazione di una chiave
Ora è molto più facile progettare un protocollo che imposti una chiave tra Alice e Bob.
Dobbiamo soltanto considerare il caso in cui dei messaggi vengono persi, ritardati o
eliminati dall’attaccante, perché il canale sicuro ci mette al riparo da ogni altro tipo di
manipolazione. Il protocollo a questo punto può essere piuttosto semplice. Alice chiede
al server di chiavi di impostare una chiave tra lei e Bob. Il server risponde inviando una
nuova chiave KAB sia ad Alice che a Bob; può anche inviare il messaggio a Bob tramite
Alice, per non comunicare direttamente con Bob. In questo caso Alice assume un ruolo
equivalente a quello di un router di rete che gestisce un canale sicuro tra il server di
chiavi e Bob.
Questo pone un’unica limitazione al sistema: Bob deve eseguire il protocollo di negoziazione della chiave con il server di chiavi prima che Alice chieda al server di impostare
una chiave condivisa con lei e Bob. Ciò può costituire un problema o no a seconda delle
circostanze esatte, e da queste dipendono anche le possibili soluzioni.

17.3.3 Rinnovo della chiave
Come tutte le chiavi anche K'A deve avere una durata limitata. È facile impostare questo
meccanismo, perché Alice può sempre rieseguire il protocollo di negoziazione della chiave
(utilizzando la chiave originale KA per l’autenticazione) per impostare una chiave K'A
nuova. Una durata di qualche ora appare ragionevole per la maggior parte dei casi.
Poiché è sempre possibile rinnovare la chiave, non è necessario che il server di chiavi
memorizzi lo stato del canale sicuro in modo affidabile. Supponete che il server di
chiavi si blocchi e perda tutte le informazioni di stato. Purché ricordi KA (e le chiavi
corrispondenti per gli altri utenti), non c’è problema, eseguire nuovamente il protocollo
di negoziazione della chiave tra il server e ciascun utente. Quindi, anche se il server di
chiavi non è privo di informazioni sullo stato, non è necessario che modifichi il suo stato
di lungo termine, ovvero la parte che è memorizzata su memoria non volatile, durante
l’esecuzione dei protocolli.

17.3.4 Altre proprietà
Forse la nostra soluzione non è più semplice di Kerberos dal punto di vista dell’implementazione, ma lo è dal punto di vista concettuale. Il canale sicuro facilita notevolmente
la sorveglianza di tutte le possibili linee di attacco contro il protocollo. Utilizzando il
protocollo di negoziazione della chiave e il canale sicuro abbiamo già progettato un buon
esempio che mostra come la modularità possa aiutare nella progettazione di protocolli
crittografici.
L’uso del protocollo di negoziazione della chiave per impostare il canale sicuro ha un
altro vantaggio: la forward secrecy. Se la chiave di Alice KA oggi viene compromessa, le
vecchie chiavi K'A sul canale sicuro non vengono rivelate, perciò tutte le comunicazioni
precedenti rimangono sicure.
Nelle parti precedenti di questo libro abbiamo fornito un esempio dettagliato di progettazione delle funzioni crittografiche discusse. Non lo facciamo qui, e nemmeno nel
prosieguo del testo. L’aspetto crittografico è relativamente semplice e potremmo certamente descrivere un sistema per server di chiavi, ma non sarebbe molto utile. Progettare

Server di chiavi   247

sistemi di gestione delle chiavi significa più che altro raccogliere un insieme di requisiti
adatti alla particolare applicazione considerata e realizzare l’interfaccia utente, mentre
la crittografia resta in secondo piano. Per poter spiegare le scelte di progettazione di
un esempio concreto, dovremmo inventare e documentare un’intera struttura sociale e
organizzativa in cui farlo operare, l’ambiente in cui nascono le minacce e l’applicazione
che necessita della gestione delle chiavi.

17.4 Che cosa scegliere
Se volete implementare un server di chiavi centrale, vi consigliamo di scegliere Kerberos,
se possibile. È molto diffuso e ben noto.
Nelle situazioni in cui Kerberos non è adatto, dovrete progettare e realizzare qualcosa
di simile alla soluzione che abbiamo descritto qui, ma sarà un compito pesante. Nei tipi
più comuni di applicazioni crittografiche che conosciamo, potete far conto che la realizzazione del server di chiavi richieda un tempo pari a quello necessario per costruire
l’intera applicazione. La discussione svolta in questo capitolo dovrebbe fornirvi almeno
un utile orientamento.

17.5 Esercizi
Esercizio 17.1 Per il protocollo del Paragrafo 17.3, qual è una durata ragionevole per
le chiavi K'A? Perché? Perché potrebbero nascere dei problemi con una durata più lunga?
Che problemi potrebbero presentarsi con una durata più breve?
Esercizio 17.2 Per il protocollo del Paragrafo 17.3, in che modo un attaccante potrebbe essere in grado di scoprire K'A prima che scada? Che cosa potrebbe fare con tale
informazione? Che cosa non potrebbe fare con tale informazione?
Esercizio 17.3 Per il protocollo del Paragrafo 17.3, in che modo un attaccante potrebbe essere in grado di scoprire K'A dopo che scada? Che cosa potrebbe fare con tale
informazione? Che cosa non potrebbe fare con tale informazione?
Esercizio 17.4 Per il protocollo del Paragrafo 17.3, considerate un attaccante che intercetti tutte le comunicazioni. Sarà in grado di leggere retroattivamente i dati scambiati
tra Alice e Bob, se in seguito KA e KB saranno svelate entrambe?
Esercizio 17.5 Per il protocollo del Paragrafo 17.3, un attaccante potrebbe trarre
vantaggio dal violare il protocollo forzando il riavvio del server di chiavi?
Esercizio 17.6 Per il protocollo del Paragrafo 17.3, un attaccante potrebbe portare
un attacco di tipo denial-of-service contro due parti che desiderano comunicare, e se sì,
come?
Esercizio 17.7 Per il protocollo del Paragrafo 17.3, esistono norme o rischi legali
attinenti al fatto di affidare al server di chiavi la generazione di KAB? Esistono cose che
Alice e Bob non direbbero in una situazione in cui il server di chiavi genera KAB e che
invece direbbero se la chiave fosse conosciuta soltanto a loro due?

Capitolo 18

Le PKI dei sogni

In questo capitolo presentiamo le PKI, spiegando che
cosa sono e come risolvono il problema della gestione
delle chiavi a livello teorico, dato che è importante
comprenderlo. Nel capitolo seguente parleremo dei
problemi delle PKI che si incontrano nell’utilizzo
concreto, ma in questo capitolo abbiamo scelto di
restare nel mondo dei sogni in cui una PKI risolve
tutti i problemi.

Sommario
18.1 Breve panoramica
sulle PKI
18.2 Esempi di PKI
18.3 Altri elementi
18.4 Riepilogo
18.5 Esercizi

18.1 Breve panoramica sulle PKI
Una PKI (Public-Key Infrastructure) è un’infrastruttura
a chiave pubblica, che consente di riconoscere le chiavi pubbliche e a chi appartengono. La descrizione
classica è la seguente.
Esiste un’autorità di certificazione centrale chiamata
CA (Certification Authority) che dispone di una coppia
di chiavi pubblica/private (per esempio una coppia di
chiavi RSA) e diffonde a tutti la chiave pubblica. Assumeremo che chiunque conosca la chiave pubblica
della CA. Poiché questa chiave rimane inalterata per
lunghi periodi di tempo, è facile diffonderla a tutti.
Per entrare nella PKI, Alice genera la propria coppia
di chiavi pubblica/privata; mantiene la chiave privata
segreta, e porta la chiave pubblica PKA alla CA dicendo:“Ciao, sono Alice e PKA è la mia chiave pubblica”.
La CA verifica che Alice sia chi afferma di essere e
quindi firma un attestato digitale che dice qualcosa
del tipo:“La chiave PKA appartiene ad Alice”. Questo
attestato firmato si chiama certificato e certifica che la
chiave appartiene ad Alice.

250   Capitolo 18

Ora, se Alice vuole comunicare con Bob, può inviargli la propria chiave pubblica e il proprio
certificato. Bob ha la chiave pubblica della CA, perciò può verificare la firma sul certificato.
Se Bob si fida della CA, si fida anche del fatto che PKA appartiene realmente ad Alice.
Utilizzando le stesse procedure, Bob ottiene la propria chiave pubblica certificata dalla CA e
invia chiave e certificato ad Alice. Ora Alice conosce la chiave pubblica di Bob e viceversa.
Queste chiavi a loro volta possono essere utilizzate per eseguire il protocollo di negoziazione
della chiave in modo da stabilire una chiave di sessione per comunicazioni sicure.
Il requisito fondamentale è un’autorità di certificazione centrale di cui tutti si fidino.
Ogni utente deve ottenere la propria chiave pubblica certificata, e ognuno deve conoscere la chiave pubblica della CA. A questo punto, chiunque può comunicare in modo
sicuro con chiunque altro.
Sembra tutto piuttosto semplice.

18.2 Esempi di PKI
Per capire meglio la parte restante di questo capitolo, è utile considerare prima alcuni
esempi di come si possano implementare e usare le PKI.

18.2.1 La PKI universale
Il sogno di tutti è una PKI universale. Una grande organizzazione, come la posta centrale, certifica la chiave pubblica di tutti. Il bello di questa soluzione è che ognuno ha
bisogno di una sola chiave certificata, che potrà usare per ogni applicazione. Poiché tutti
si fidano della posta centrale, o comunque dell’organizzazione che diventa l’autorità di
certificazione universale, chiunque può comunicare in sicurezza con chiunque altro e
tutti vivono felici e contenti.
Se la nostra descrizione vi sembra simile a una fiaba, avete proprio ragione. In realtà non
esiste una PKI universale, e non esisterà mai.

18.2.2 Accesso con VPN
Un esempio più realistico è quello di un’azienda che dispone di una VPN (Virtual Private
Network) per consentire ai dipendenti di accedere alla rete aziendale da casa o da una
stanza di albergo quando sono in viaggio. I punti di accesso della VPN devono essere
in grado di riconoscere le persone autorizzate ad accedere e il livello di accesso di cui
dispongono. Il reparto IT dell’azienda assume il ruolo dell’autorità di certificazione e
fornisce a ogni dipendente un certificato che gli consente di farsi riconoscere dai punti
di accesso della VPN.

18.2.3 Banca elettronica
Una banca vuole consentire ai propri clienti di eseguire transazioni finanziarie sul proprio sito web. In questa applicazione l’identificazione del cliente è fondamentale, come
anche la capacità di produrre prove accettabili in tribunale. La banca stessa può fare da
autorità di certificazione e certificare le chiavi pubbliche dei suoi clienti.

Le PKI dei sogni   251

18.2.4 Sensori di una raffineria
Le raffinerie sono complessi industriali molto grandi. Diffusi tra chilometri di tubi e
strade di accesso ci sono centinaia di sensori che misurano per esempio temperatura,
portata e pressione. Un attacco che falsifichi i dati costituirebbe un grave pericolo per
l’impianto. Potrebbe essere non troppo difficile inviare dati falsi al centro di controllo,
inducendo gli operatori a intraprendere azioni sbagliate fino a generare un’esplosione.
Perciò, è imperativo che il centro di controllo riceva le misurazioni corrette effettuate
dai sensori. Si possono utilizzare tecniche di autenticazione standard per assicurarsi che
i dati dei sensori non siano stati alterati, ma per essere certi che i dati provengano effettivamente dai sensori, è necessaria un’infrastruttura di gestione delle chiavi. L’azienda
può fare da CA e realizzare una PKI per tutti i sensori, in modo che ognuno possa essere
identificato dal centro di controllo.

18.2.5 Circuiti di carte di credito
Un circuito di carte di credito è un’organizzazione che prevede un accordo di cooperazione tra migliaia di banche diffuse in tutto il mondo.Tutte queste banche devono essere
in grado di scambiare pagamenti. Un utente che possiede una carta di credito della banca
A deve essere in grado di pagare un commerciante che lavora con la banca B. La banca
A dovrà accordarsi in qualche modo con la banca B, e questo richiede comunicazioni
sicure. Una PKI consente a tutte le banche di identificarsi a vicenda e di effettuare transazioni sicure. In questo scenario, il circuito di carte di credito può assumere il ruolo di
CA che certifichi le chiavi di ogni banca.

18.3 Altri elementi
Nella vita reale le cose si fanno più complicate, perciò spesso si utilizzano varie estensioni
allo schema di base della PKI.

18.3.1 Certificati multilivello
Spesso la CA è suddivisa in più parti. Per esempio, la sede centrale di un circuito di carte
di credito non certificherà direttamente ogni banca, ma avrà sedi regionali per trattare
con le singole banche locali. Si ha quindi una struttura di certificati a due livelli. La CA
centrale firma un certificato per la chiave pubblica della CA regionale, che dice qualcosa del tipo: “La chiave PKX appartiene all’ufficio regionale X e consente di certificare
altre chiavi”. Ogni ufficio regionale può quindi certificare le chiavi di singole banche. Il
certificato della chiave di una banca è costituito da due messaggi firmati: la delega della
CA centrale che autorizza la chiave dell’ufficio regionale e la certificazione della chiave
della banca, concessa dall’ufficio regionale. Si parla di catena di certificati, che può essere
applicata fino a qualsiasi numero di livelli.
Le strutture di certificati multilivello possono essere molto utili. In sostanza consentono
di suddividere la funzione di CA in una gerarchia, che per la maggior parte delle organizzazioni ne facilita la gestione. Uno svantaggio di questa struttura è che i certificati
diventano più grandi e richiedono più calcoli per la verifica, ma si tratta di un costo

252   Capitolo 18

relativamente piccolo nella maggior parte dei casi. Un altro svantaggio è che ogni CA
in più aggiunta al sistema fornisce un altro possibile punto di attacco, riducendo così la
sicurezza complessiva del sistema.
Un modo per ridurre lo svantaggio dei certificati multilivello che aumentano di dimensioni sarebbe quello di comprimere la gerarchia dei certificati, ma non lo abbiamo mai
incontrato nella pratica. Per continuare con l’esempio, una volta che la banca ha ottenuto
il proprio certificato a due livelli, potrebbe inviarlo alla CA centrale; questa verifica il
certificato a due livelli e risponde con un singolo certificato per la chiave della banca,
utilizzando la chiave della CA principale. In questo modo la gerarchia delle chiavi viene
compressa e il costo prestazionale di aggiungere altri livelli gerarchici si riduce di molto.
Ancora una volta, però, aggiungere altri livelli potrebbe non essere una buona idea, dato
che raramente le strutture gerarchiche a più livelli sono efficaci.
La concatenazione di certificati come appena proposto va gestita con attenzione. Aumenta la complessità, e di conseguenza aumentano i rischi. Ecco un esempio. I siti sicuri
su Internet utilizzano un sistema di PKI per consentire ai browser di identificare il sito
corretto. Nella pratica questo sistema non è molto sicuro, se non altro perché la maggior
parte degli utenti non controlla il nome del sito web che utilizza.Tempo fa si manifestò
un bug fatale in una libreria che valida i certificati su tutti i sistemi operativi di Microsoft;
ogni elemento della catena di certificati contiene un flag che specifica se la chiave che
certifica è una chiave di CA o no. Le chiavi di CA possono certificare altre chiavi, mentre
le chiavi normali non possono farlo, ed è una differenza importante. Sfortunatamente la
libreria in questione non controllava questo flag, perciò un attaccante poteva acquistare
un certificato per il dominio nastyattacker.com e utilizzarlo per firmare un certificato per
amazon.com. Microsoft Internet Explorer utilizzava quella libreria, perciò avrebbe accettato
la certificazione di una falsa chiave di Amazon, fornita da nastyattacker.com, visualizzando
il sito falso come se fosse davvero quello di Amazon. Un sistema di sicurezza operante a
livello mondiale, costato una fortuna, fu messo al tappeto da un piccolissimo bug presente
in un’unica libreria. Una volta reso noto il bug fu rilasciata una patch (servirono diversi
tentativi per correggere tutti i problemi), ma questo rimane un buon esempio di come
un bug minore possa distruggere la sicurezza di un intero sistema.

18.3.2 Scadenza
Nessuna chiave crittografica dovrebbe essere utilizzata per un periodo indefinito; c’è
sempre un rischio che sia compromessa. Cambiando la chiave a intervalli regolari si
può rimediare anche a questo, per quanto lentamente. Un certificato non dovrebbe mai
essere valido per sempre, perché sia la chiave della CA sia quella pubblica da certificare
hanno una scadenza.
A parte questi motivi legati alla crittografia, la scadenza è importante anche per mantenere aggiornate le informazioni. Quando un certificato scade, sarà necessario emetterne
un altro, e così si crea un’opportunità di aggiornare le informazioni che contiene. Un
periodo di scadenza tipico va da pochi mesi a pochi anni.
Quasi tutti i sistemi di gestione dei certificati includono data e ora di scadenza, e nessuno dovrebbe accettare un certificato scaduto. Ecco perché gli utenti di una PKI hanno
bisogno di un orologio.
Molti progetti includono nel certificato altri dati. Spesso i certificati hanno una data di
inizio della validità, oltre alla data di scadenza. Esistono anche altri dati che indicano

Le PKI dei sogni   253

le diverse classi di certificati, numeri di serie, data e ora di emissione e così via; alcuni
sono utili, altri no.
Il formato più diffuso per i certificati è X.509 v3, davvero molto complicato. Consigliamo
di consultare la guida di Peter Gutmann [58] per una trattazione di X.509. Se lavorate
su un sistema che non richiede l’interoperabilità con altri sistemi, potreste prendere in
considerazione l’idea di non utilizzare X.509. Naturalmente X.509 è standardizzato, e
nessuno vi potrà accusare per aver utilizzato uno standard.

18.3.3 Autorità di registrazione separata
A volte incontrerete sistemi con autorità di registrazione separata. Il problema è di carattere politico. In un’azienda è l’ufficio del personale che decide chi è un dipendente.
Ma la gestione della CA deve essere affidata al reparto IT, poiché si tratta di un compito
tecnico che il reparto non vorrà certo lasciare all’ufficio del personale.
Esistono due buone soluzioni per questo problema. La prima è quella di utilizzare una
struttura di certificati multilivello e consentire all’ufficio del personale di assumere il
ruolo di sotto-CA. Questo fornisce automaticamente le flessibilità necessaria per supportare più siti. La seconda soluzione è simile alla prima, con la differenza che, una volta
che un utente dispone di un certificato a due livelli, lo scambia con uno di un solo
livello rivolgendosi alla CA centrale. Così si elimina il lavoro necessario per controllare
un certificato a due livelli ogni volta che viene utilizzato, con il costo di aggiungere un
semplice protocollo a due messaggi.
Una soluzione sbagliata è quella di aggiungere una terza parte al protocollo critograffico. Le specifiche di progetto indicheranno la CA e un’altra parte che potrebbe essere
chiamata RA (Registration Authority) o qualcosa di simile. CA e RA sono trattate come
entità del tutto separate; questo può comportare oltre 100 pagine di documentazione
aggiuntiva per il sistema, cosa negativa di per sé. Poi c’è la necessità di specificare l’interazione tra RA e CA. Abbiamo visto protocolli a tre parti in cui la RA autorizza la CA
a emettere un certificato. È un buon esempio del problema che nasce quando i requisiti
dell’utente si impongono nei confronti di una soluzione tecnica. I requisiti dell’utente
specificano soltanto il comportamento esterno di un sistema. L’azienda necessita di
funzionalità separate per i reparti del personale e di information technology. Questo
tuttavia non significa che il software debba prevedere codice differente per i reparti del
personale e IT. In molti casi, e certamente anche in quello considerato qui, i due reparti
possono utilizzare in gran parte la stessa funzionalità, e quindi lo stesso codice. L’uso di
un unico insieme di funzioni di certificato porta a un progetto più semplice, più economico, più potente e più flessibile di quello basato direttamente sui requisiti originali,
che includevano una CA e una RA. Uno schema di CA a due livelli consente al reparto
del personale e al reparto IT di condividere gran parte del codice e dei protocolli. Le
differenze, in questo caso, stanno principalmente nell’interfaccia utente e dovrebbero
essere facili da implementare. Questo si traduce in qualche centinaia di righe di codice
in più, non in centinaia di pagine di documentazione aggiuntive, che si trasformano in
decine di migliaia di righe di codice.

254   Capitolo 18

18.4 Riepilogo
In questo capitolo abbiamo descritto un sogno, che tuttavia ha una grande importanza.
Le PKI rappresentano il fondamento della gestione delle chiavi per gran parte del nostro
settore. Le persone sono state indotte a credere in questo sogno, che gli appare talmente
ovvio da non richiedere spiegazioni. Per poterle capire, dovete capire il sogno delle
PKI, perché molto di ciò che essi dicono rientra nel contesto del sogno. Ed è così bello
pensare di avere una soluzione al problema di gestione della chiave...

18.5 Esercizi
Esercizio 18.1 Supponete che una CA sia maligna. Che cosa può fare di male?
Esercizio 18.2 Supponete che sia in uso una PKI universale. Possono nascere problemi
di sicurezza a causa dell’uso di questa singola PKI in più applicazioni?
Esercizio 18.3 Quali norme o sfide organizzative potrebbero impedire o evitare la
messa in opera di una PKI universale per tutto il mondo?
Esercizio 18.4 Oltre agli esempi del Paragrafi 18.2.2 – 18.2.5, fornite tre scenari di
esempio per cui si potrebbe utilizzare una PKI.

Capitolo 19

Le PKI nella realtà

Le PKI sono un concetto molto utile, ma che presenta
alcuni problemi fondamentali. Non in teoria, ma
talvolta la teoria si rivela molto diversa dalla pratica.
Nel mondo reale le PKI non funzionano come nello
scenario ideale considerato nel Capitolo 18. Ecco
perché molti aspetti di cui si parla molto, in relazione
alle PKI, non sono mai stati tradotti in realtà.
Quando parliamo di PKI, la nostra visione non si
limita all’email e al Web, ma è molto più ampia.
Consideriamo anche il ruolo delle PKI nell’autorizzazione e in altri sistemi.

Sommario
19.1 Nomi
19.2 Autorità
19.3 Fiducia
19.4 Autorizzazione
indiretta
19.5 Autorizzazione diretta
19.6 Sistemi di credenziali
19.7 Il sogno rivisitato

19.1 Nomi
Iniziamo con un problema relativamente semplice: il
concetto di nome. La PKI lega la chiave pubblica di
Alice al suo nome. Che cos’è un nome?
Cominciamo con uno scenario semplice. In un piccolo villaggio, tutti si conoscono di vista. Chiunque
ha un nome, che può essere univoco o sarà reso tale.
Se ci sono due Luigi, presto uno loro verrà chiamato
Gigi. Per ogni nome c’è una sola persona, ma una
persona può avere più nomi. Gigi potrebbe essere
chiamato anche Comandante o Signor Rossi.
Il nome di cui stiamo parlando qui non è quello
che appare sui documenti legali, ma quello utilizzato
per indicare una persona. Un nome è in sostanza un
significante utilizzato per indicare una persona, o più
in generale un’entità. Il vostro nome “ufficiale” è solo
uno tra tanti nomi diversi, e per molti probabilmente
è anche quello utilizzato di meno.

19.8 La revoca
19.9 In conclusione, a che
cosa serve una PKI?
19.10 Che cosa scegliere
19.11 Esercizi

256   Capitolo 19

Quando il villaggio cresce e diventa una piccola città, il numero di abitanti diventa
talmente grande che è impossibile conoscerle tutti. I nomi iniziano a perdere la loro
associazione immediata con una persona. Forse c’è un unico L. Rossi in città, ma non
è detto che lo conosciate. I nomi cominciano così a condurre una vita propria, separata
dalla persona reale. Cominciate a parlare di persone che non avete mai incontrato realmente. Magari vi capiterà di parlare al bar del ricco Signor Rossi che è appena arrivato
in città e farà da sponsor per la squadra di pallacanestro del liceo il prossimo anno. Due
settimane dopo vi accorgerete che è la stessa persona che è entrata nella vostra squadra
di calcio due mesi fa, e che ora conoscete come Luigi. Le persone continuano ad avere
più nomi, dopo tutto. Solo, non è così ovvio individuare quali nomi vadano insieme e
a quale persona si riferiscano.
Quando la città cresce e diventa una metropoli, le cose cambiano ancora. Presto arriverete a conoscere soltanto un piccolo sottoinsieme degli abitanti. Inoltre, i nomi non
saranno più univoci. Non serve a molto conoscere il nome Luigi Rossi, se nella metropoli ci sono centinaia di persone che si chiamano così. Il significato di un nome inizia a
dipendere dal contesto. Alice potrebbe conoscere tre Luigi, ma quando parla di “Luigi”
al lavoro, è chiaro dal contesto che intende Luigi dell’ufficio vendite. Più tardi, a casa, lo
stesso nome potrebbe indicare Luigi il figlio del vicino. La relazione tra un nome e una
persona diventa ancora più sfocata.
Ora considerate Internet. Più di un miliardo di persone sono online. Che cosa significa
il nome “Luigi Rossi” nella rete? Quasi niente: ce ne sono troppi. Perciò, invece dei
nomi tradizionali si utilizzano gli indirizzi email. Ora comunicate con lrossi533@yahoo.
it. Questo è certamente un nome univoco, ma nella pratica non corrisponde a una
persona nel senso di qualcuno che prima o poi incontrerete. Anche se potreste trovare
informazioni come l’indirizzo di casa e il numero di telefono, è sempre qualcuno che
potrebbe vivere all’altro capo del pianeta. Non vi incontrerete mai di persona, a meno
che non vogliate proprio organizzare un incontro. A questo punto non desta sorpresa
il fatto che molti assumano diverse personalità online. E come sempre, ogni persona ha
più nomi. La maggior parte degli utenti utilizza diversi indirizzi di posta elettronica (tra
gli autori di questo libro ne contiamo più di dieci). Tuttavia, è estremamente difficile
determinare se due indirizzi di posta elettronica si riferiscono alla stessa persona. E a
complicare ulteriormente le cose, esistono casi in cui una persona condivide un indirizzo
email con un’altra, perciò quel “nome” si riferisce a entrambe.
Esistono grandi organizzazioni che tentano di assegnare nomi a tutti. Le più note sono
i governi. La maggior parte degli stati richiede che ogni persona abbia un singolo nome
ufficiale, utilizzato nel passaporto e in altri documenti ufficiali. Il nome in sé non è
univoco, perché possono esserci più persone con quel nome, perciò nella pratica viene
spesso esteso con dati quali l’indirizzo di casa, il numero della patente di guida, la data
di nascita. Ma questo non garantisce ancora di ottenere un identificatore univoco per
una persona (il numero della patente di guida è univoco, ma non tutti hanno la patente).
Inoltre, molti di questi identificatori possono cambiare durante la vita di una persona,
come avviene per l’indirizzo, il numero della patente e perfino il sesso. Praticamente
l’unica cosa che non cambia è la data di nascita, ma ciò è compensato dal fatto che molte
persone mentono su quel dato.
Se pensate che ogni persona abbia un nome ufficiale stabilito dallo stato, vi sbagliate.
Alcune persone non hanno non anno alcuna cittadinanza e sono privi di documenti.
Altri hanno doppia cittadinanza, con due stati che tentano ciascuno di stabilire un nome
ufficiale, e per vari motivi potrebbero non trovare un accordo. I due stati potrebbero

Le PKI nella realtà   257

utilizzare alfabeti diversi, nel qual caso i nomi non potrebbero coincidere. Alcuni paesi
richiedono un nome adatto alla lingua nazionale e sostituiscono i nomi stranieri con
altri simili e “adatti” alla lingua.
Per evitare confusione, molti paesi assegnano agli individui numeri o codici univoci,
come il codice fiscale in Italia o il numero della previdenza sociale (SSN, Social Security
Number) negli Stati Uniti. Il senso è quello di fornire a un individuo un nome univoco
e fissato, in modo da poterne tracciare e ricollegare le azioni. Questi schemi di numerazione ottengono buoni risultati, ma hanno anch’essi dei difetti. Il collegamento tra la
persona e il numero assegnato non è molto stretto, e in certi settori dell’economia vi è un
ampio utilizzo di numeri falsi. Inoltre, poiché questi schemi di numerazione funzionano
all’interno di ciascun paese, non forniscono copertura globale, né univocità globale.
Un altro aspetto dei nomi merita di essere citato. In Europa ci sono leggi sulla privacy che
limitano i tipi di informazioni personali che un’organizzazione può registrare. Per esempio,
un supermercato non può chiedere, registrare o elaborare in altro modo un codice fiscale
da utilizzare per un programma fedeltà, a meno di autorizzazioni specifiche. Questo fatto
limita il riutilizzo degli schemi di assegnazione dei nomi imposti dallo stato.
Quale nome si dovrebbe utilizzare in una PKI? Molte persone hanno molti nomi diversi, e
questo diventa un problema. Forse Alice vuole avere due chiavi, una per le comunicazioni
di lavoro e una per quelle personali. Ma potrebbe utilizzare il nome da nubile per il lavoro
e il nome da sposata per la corrispondenza privata. Casi come questi fanno nascere seri
problemi se si cerca di realizzare una PKI universale. Questo è uno dei motivi per cui piccole
PKI specifiche di un’applicazione funzionano meglio di un’unica enorme infrastruttura.

19.2 Autorità
Chi è questa CA che vanta l’autorità di assegnare delle chiavi a dei nomi? A che cosa si
deve l’autorità della CA rispetto a questi nomi? Chi decide se Alice è una dipendente a
cui concedere l’accesso alla VPN di una banca o una cliente con accesso limitato?
Per la maggior parte dei nostri esempi, queste domande trovano subito risposta. Il datore
di lavoro sa chi è un dipendente e chi no. La banca conosce i suoi clienti. Questo ci
fornisce una prima indicazione di quale organizzazione dovrebbe essere la CA. Sfortunatamente, pare che non esista una fonte di autorità per la PKI universale, e questo è
uno dei motivi per cui una PKI universale non può funzionare.
Quando pianificate una PKI, dovete pensare a chi è autorizzato a emettere i certificati.
Per esempio, è facile per un’azienda assumere l’autorità rispetto ai propri dipendenti.
L’azienda non decide quale sia il nome del dipendente, ma conosce il nome con cui il
dipendente è conosciuto all’interno dell’azienda. Se “Gigi Rossi” in realtà si chiama Luigi, non
importa. Il nome “Gigi Rossi” va benissimo nel contesto dei dipendenti dell’azienda.

19.3 Fiducia
La gestione delle chiavi è il problema più difficile nel campo della crittografia, e un sistema
PKI è uno dei migliori strumenti che abbiamo a disposizione per risolverlo. Ma tutto
dipende dalla sicurezza della PKI, e quindi dall’affidabilità della CA. Pensate ai danni che
si potrebbero causare se la CA iniziasse a falsificare i certificati. La CA può impersonare
chiunque nel sistema, e in questo modo la sicurezza sarebbe totalmente violata.

258   Capitolo 19

Una PKI universale sarebbe molto attraente, ma cade proprio sulla fiducia. Se foste una
banca e doveste comunicare con i vostri clienti, vi fidereste di una dot-com con sede
all’altro capo del pianeta? O anche della burocrazia pubblica locale? Quanto denaro rischiereste di perdere se la CA dovesse fare qualcosa di terribile? Quanta responsabilità è
disposta ad assumersi la CA? Le normative locali sull’attività bancaria consentono l’utilizzo
di una CA estera? Sono tutti problemi enormi. Pensate solo ai danni che potrebbero
essere provocati se la chiave privata della CA fosse pubblicata su un sito web.
Ora ragioniamo in termini tradizionali. La CA è l’organizzazione che distribuisce le chiavi
degli edifici. La maggior parte dei grandi edifici commerciali dispone di guardie giurate,
e la maggior parte delle guardie giurate proviene da un’agenzia di sicurezza esterna. Le
guardie verificano che le regole siano rispettate: un lavoro piuttosto semplice. Tuttavia,
decidere come assegnare le chiavi non è un compito che normalmente viene affidato a un
a fornitore esterno, perché costituisce una parte fondamentale della politica di sicurezza.
Per lo stesso motivo, anche la funzione di CA non dovrebbe essere esternalizzata.
Non esiste al mondo un’organizzazione di cui tutti si fidino. Non ne esiste nemmeno una
di cui si fidi la maggior parte delle persone. Quindi, non ci sarà mai una PKI universale.
La conclusione logica è che dovremo utilizzare molte piccole PKI, ed è proprio questa
la soluzione adottata per i nostri esempi. La banca può essere la propria CA; dopo tutto,
la banca si fida di se stessa, e i clienti si fidano della banca dato che vi depositano i loro
soldi. Un’azienda può essere la propria CA per la VPN, e anche un circuito di carte di
credito può gestire la propria CA.
Un aspetto interessante è che le relazioni di fiducia utilizzate dalle CA sono quelle già
esistenti e sono basate su relazioni contrattuali. È sempre così quando si progettano sistemi
crittografici: le relazioni di fiducia sono tutte basate su relazioni contrattuali.

19.4 Autorizzazione indiretta
A questo punto incontriamo un grande problema delle classiche PKI “dei sogni”. Considerate i sistemi di autorizzazione. La PKI associa chiavi a nomi, ma la maggior parte
dei sistemi non è interessata al nome della persona. Il sistema della banca vuole sapere
quale transazioni autorizzare. La VPN vuole sapere a quali directory consentire l’accesso. Nessuno di questi sistemi si interessa di chi possiede la chiave, ma solo di che cosa è
autorizzato a fare il proprietario della chiave.
La maggior parte dei sistemi utilizza qualche tipo di elenco di controllo degli accessi,
o ACL (Access Control List). Si tratta semplicemente di un database che specifica chi è
autorizzato a fare cosa. A volte è ordinato per utente (per esempio, Bob è autorizzato
a fare le seguenti operazioni: accedere ai file della /homes/bob, utilizzare la stampante
dell’ufficio, accedere al file server), ma nella maggior parte dei casi è indicizzato per
azione (per esempio, gli addebiti su questo conto devono essere autorizzati da Bob o
Betty). Spesso c’è la possibilità di creare gruppi di persone per semplificare gli elenchi
ACL, ma la funzionalità di base rimane la stessa.
Finora abbiamo tre diversi oggetti: una chiave, un nome e un permesso di fare qualcosa.
Il sistema vuole sapere quale chiave autorizza quale azione, o in altre parole se una particolare chiave ha un particolare permesso. La PKI risolve il problema associando le chiavi
ai nomi e utilizzando un ACL per associare i nomi ai permessi; è un metodo tortuoso
che introduce altri punti di attacco [45].

Le PKI nella realtà   259

Il primo punto di attacco è il nome–chiave certificato fornito dalla PKI. Il secondo è il
database ACL che associa i nomi ai permessi. Il terzo è la confusione dei nomi: come si
fa a stabilire se il nome elencato nell’ACL è lo stesso citato nel certificato della PKI? E
come si fa per evitare di assegnare a due persone diverse lo stesso nome?
Se analizzate la situazione, vedrete chiaramente che il progetto tecnico ha seguito la
formulazione ingenua dei requisiti. Le persone considerano il problema in termini di
identificare chi detiene la chiave e chi dovrebbe avere accesso, come fanno le guardie
giurate di un edificio commerciale. I sistemi automatizzati possono utilizzare un approccio molto più diretto. A una serratura non interessa chi ha la chiave, lo fa entrare
chiunque sia.

19.5 Autorizzazione diretta
Una soluzione molto migliore è generalmente quella di assegnare direttamente i permessi
alla chiave, utilizzando la PKI. Il certificato non collega più la chiave a un nome, ma a
un insieme di permessi [45].
Ora tutti i sistemi che utilizzano i certificati PKI possono decidere direttamente se consentire l’accesso o meno: esaminano il certificato presentato e verificano se la chiave ha
i permessi appropriati. Diretto e semplice.
L’autorizzazione diretta elimina l’ACL e i nomi dal processo di autorizzazione, con i
corrispondenti punti di attacco. Alcuni dei problemi naturalmente ricompariranno nel
punto in cui vengono emessi i certificati. Qualcuno deve decidere chi è autorizzato a
fare cosa, e garantire che questa decisione sia codificata correttamente nei certificati. Il
database di tutte queste decisioni diventa l’equivalente del database ACL, ma è meno
esposto agli attacchi. È facile distribuire questo database alle persone che prendono le
decisioni, eliminando il database ACL centrale con le sue vulnerabilità. I responsabili
delle decisioni possono semplicemente emettere il certificato appropriato per l’utente,
senza un’ulteriore infrastruttura che costituisca un punto critico per la sicurezza. Così
si riduce anche la dipendenza dai nomi, perché i responsabili delle decisioni sono localizzati molto più in basso nella gerarchia e devono gestire un numero di persone molto
minore; spesso conoscono gli utenti personalmente, o almeno di vista, cosa che riduce
di molto i problemi di confusione dei nomi.
E così possiamo fare a meno dei nomi nei certificati?
In effetti no. Benché i nomi non siano utilizzati durante le normali attività, dobbiamo
fornire dati di log per compiti di controllo e simili. Supponete che la banca abbia appena
elaborato il pagamento di uno stipendio, autorizzato da una delle quattro chiavi che ha
autorità di pagamento per il conto. Tre giorni dopo, il direttore finanziario chiama la
banca e chiede perché sia stato effettuato il pagamento. La banca sa che il pagamento è
stato autorizzato ma non può fornire al direttore finanziario qualche migliaio di bit della
chiave pubblica. Ecco perché dobbiamo ancora inserire un nome in ogni certificato. In
questo modo la banca può dire al direttore finanziario che la chiave usata per autorizzare
il pagamento apparteneva a “L. Rossi”, quanto basta perché sia possibile capire che cosa
è successo. Ma il punto importante è che i nomi qui servono solo a persone umane. Il
computer non prova mai a determinare se due nomi coincidono, o a quale persona appartenga un nome. Gli umani sono molto più bravi nel trattare i nomi indistinti, mentre
i computer amano entità semplici e ben specificate come gli insiemi di permessi.

260   Capitolo 19

19.6 Sistemi di credenziali
Portando ancora più avanti il principio precedentemente descritto si ottiene un sistema
di credenziali completo, una sorta di super PKI del crittografo. In pratica, ognuno ha
bisogno di una credenziale, in forma di certificato firmato, per ogni azione che intende
eseguire.
Se Alice ha una credenziale che le consente di leggere e scrivere un particolare file, può
delegare la propria autorità (in tutto o in parte) a Bob. Per esempio, potrebbe firmare un
certificato per la chiave pubblica di Bob, che dica qualcosa del tipo: “La chiave PKBob è
autorizzata a leggere il file X per autorità delegata dalla chiave PKAlice”. Se Bob vuole
leggere il file X, deve presentare questo certificato e una prova del fatto che Alice ha
accesso in lettura al file X.
In un sistema di credenziali si possono aggiungere altre funzionalità. Alice potrebbe
limitare il tempo di validità della delega specificandolo nel certificato. Potrebbe anche
limitare la possibilità di Bob di delegare l’autorità di leggere il file X. Questa in effetti è
una funzionalità richiesta frequentemente, ma crediamo che non sia sempre una buona
scelta. Limitando la possibilità di Bob di delegare la propria autorità, in pratica lo si
invita a eseguire un programma proxy in modo che altre persone possano utilizzare la
sua credenziale per accedere a una risorsa. Simili programmi minano l’infrastruttura di
sicurezza e dovrebbero essere banditi, ma lo si può fare soltanto se non ci sono ragioni
operative che richiedano di eseguire un proxy. E ci sono sempre ragioni operative per
cui qualcuno necessita di delegare l’autorità.
In teoria un sistema di credenziali è estremamente potente e flessibile, ma nella pratica
questi sistemi sono utilizzati di rado. I motivi sono diversi. In primo luogo, i sistemi di
credenziali sono piuttosto complessi e possono comportare un sovraccarico visibile. L’autorità di accedere a una risorsa potrebbe dipendere da una catena di cinque o sei certificati,
ognuno dei quali deve essere trasmesso e verificato. Il secondo problema è che i sistemi
di credenziali favoriscono una microgestione dell’accesso. È troppo facile suddividere le
autorità in porzioni via via più piccole e gli utenti finiranno per passare troppo tempo
a decidere esattamente quanta autorità delegare ai vari colleghi. Questo tempo è spesso
sprecato, ma un problema ancora più grave è la perdita di tempo del collega quando si
accorge che non ha autorità sufficiente per svolgere il proprio lavoro.
Forse questo problema di microgestione potrebbe essere risolto istruendo meglio gli
utenti e migliorando le interfacce, ma sembra comunque un problema aperto. Alcuni
utenti lo evitano delegando (quasi) tutta la loro autorità a chiunque necessiti di qualsiasi
tipo di accesso, minando così l’intero sistema di sicurezza.
Il terzo problema è che occorre sviluppare un linguaggio per credenziali e delega. I
messaggi di delega devono essere scritti in un linguaggio logico che i computer possano comprendere. Questo linguaggio deve essere abbastanza potente da esprimere tutte
le funzionalità desiderate, e tuttavia sufficientemente semplice da consentire un rapido
concatenamento delle conclusioni. Deve anche essere a prova di futuro. Una volta
messo in opera un sistema di credenziali, ogni programma dovrà comprendere codice
per interpretare il linguaggio delle deleghe. L’aggiornamento a una nuova versione di
tale linguaggio potrebbe risultare molto difficoltoso, soprattutto perché le funzionalità
di sicurezza si diffondono in ogni componente del sistema. In effetti è impossibile progettare un linguaggio di delega che sia sufficientemente generico da soddisfare tutti i
requisiti futuri, poiché non sappiamo che cosa ci porterà il futuro. Questo rimane un
campo di ricerca.

Le PKI nella realtà   261

Il quarto problema dei sistemi di credenziali è che probabilmente insormontabile. Una
delega di autorità dettagliata è un concetto troppo complesso per l’utente medio. Non
pare esistere un metodo che consenta di presentare agli utenti le regole di accesso in
modo che essi siano in grado di capirle. Chiedere agli utenti di prendere decisioni in
merito a quali autorità delegare è un’impresa votata al fallimento. Lo vediamo già nel
mondo reale. In alcuni campus universitari è normale che uno studente vada al bancomat
e prelevi denaro per diverse persone. Gli altri studenti gli affidano il loro bancomat e il
codice PIN. Si tratta di un comportamento estremamente rischioso, e tuttavia è adottato
da alcune tra quelle che dovrebbero essere le persone meglio istruite della nostra società.
Nel nostro lavoro di consulenza abbiamo visitato molte aziende e talvolta ci è capitato
di dover accedere alla rete locale. È sorprendente il grado di accesso che abbiamo spesso
ottenuto. Abbiamo trovato amministratori di sistemi che ci hanno dato accesso illimitato
ai dati delle ricerche, quando tutto ciò che ci serviva era esaminare un file o due. Se gli
amministratori di sistemi faticano a capire l’importanza di questo aspetto, figuratevi gli
utenti normali.
Ai professionisti della crittografia piacerebbe molto l’idea di un sistema di credenziali,
se solo gli utenti fossero in grado di affrontarne la complessità. C’è sicuramente molto
da studiare sulle interazioni tra gli uomini e i sistemi di sicurezza.
Esiste comunque un campo in cui le credenziali sono molto utili e dovrebbero essere
obbligatorie. Se utilizzate una struttura di CA gerarchica, la CA centrale firma certificati
per le chiavi delle sotto-CA. Se questi certificati non includono dei vincoli di qualche
tipo, ogni sotto-CA riceve un potere illimitato, e questo costituisce un problema, poiché
si moltiplica il numero di punti in cui sono memorizzate chiavi di importanza critica
per il sistema.
In una struttura di CA gerarchica, il potere di una sotto-CA dovrebbe essere limitato
specificando dei vincoli nel certificato della sua chiave. Questo richiede un linguaggio
di delega, sul modello del sistema di credenziali, per le attività delle CA. Il tipo esatto di
vincoli da imporre dipende dall’applicazione. Basta riflettere su quali tipi di sotto-CA si
desiderano creare e su come limitarne il potere.

19.7 Il sogno rivisitato
In questo paragrafo riepiloghiamo tutte le critiche alle PKI riportate fin qui e presentiamo una versione rivisitata, che costituisce una rappresentazione più realistica di ciò
che una PKI dovrebbe essere.
In primo luogo, ogni applicazione ha la propria PKI con la propria CA. Nel mondo ci sono
tantissime piccole PKI e ogni utente è membro di molte PKI diverse nello stesso tempo.
L’utente deve utilizzare chiavi diverse per ciascuna PKI, dato che non può utilizzare la
stessa chiave in sistemi diversi in assenza di un’attenta coordinazione dei sistemi a livello
di progetto. L’archivio di chiavi dell’utente, quindi, conterrà decine di chiavi, e richiederà
decine di kilobyte di memoria.
Scopo principale delle PKI è quello di associare una credenziale alla chiave. La PKI
della banca associa alla chiave di Alice la credenziale che consente l’accesso al suo conto.
Oppure, la PKI dell’azienda associa alla chiave di Alice una credenziale che consente
l’accesso alla VPN. Modifiche significative delle credenziali di un utente richiedono
l’emissione di un nuovo certificato. I certificati contengono sempre il nome dell’utente,
ma principalmente per scopi di gestione e auditing.

262   Capitolo 19

Questo sogno rivisitato è molto più realistico. È anche più potente, più flessibile e più
sicuro dell’originale. Si sarebbe tentati di credere che questa versione modificata risolverà i problemi di gestione delle chiavi. Invece, nel paragrafo seguente incontreremo
il problema più difficile di tutti, che non sarà mai risolto completamente e richiederà
sempre dei compromessi.

19.8 La revoca
Il problema più difficile da risolvere in una PKI è quello della revoca.A volte un certificato
deve essere ritirato. Forse il computer di Bob è stato violato e la sua chiave privata è stata
compromessa. Forse Alice è stata trasferita in un altro reparto o addirittura licenziata.
Esistono innumerevoli casi in cui si presenta l’esigenza di revocare un certificato.
Il problema è che un certificato è soltanto un gruppo di bit. Questi bit sono stati utilizzati in molti punti e memorizzati in molti luoghi. Non è possibile fare in modo che il
mondo dimentichi il certificato, per quanto impegno ci si metta. Bruce Schneier perse
una chiave PGP più di dieci anni fa: continua a ricevere email cifrate con il certificato
corrispondente (PGP ha una propria struttura simile a una PKI ma particolare, chiamata
web of trust; per ulteriori informazioni al riguardo consigliamo di leggere [130]).
Anche solo tentare di fare in modo che il mondo dimentichi il certificato è irrealistico.
Se un ladro entra nel computer di Bob e sottrae la sua chiave privata, potete star certi
che farà anche una copia del certificato per la chiave pubblica corrispondente.
Ogni sistema ha i propri requisiti, ma in generale quelli della revoca si distinguono per
quattro variabili, elencate di seguito.
• Velocità della revoca. Qual è il tempo massimo concesso tra il comando di revoca e
l’ultimo impiego del certificato?
• Affidabilità della revoca. È accettabile che in alcune circostanze la revoca non sia
pienamente efficace? Qual è il livello di rischio residuo accettabile?
• Numero di revoche. Quante revoche dovrebbe gestire per volta il sistema di revoca?
• Connettività. L’utente può eseguire un controllo online al momento della verifica
del certificato?
Esistono tre soluzioni praticabili per il problema della revoca: elenchi delle revoche,
scadenza rapida e verifica online del certificato.

19.8.1 Elenco delle revoche
Un elenco dei certificati revocati, o CRL (Certificate Revocation List), è un database contenente
un elenco dei certificati che sono stati revocati. Chiunque voglia verificare un certificato
deve consultare tale database per controllare se è stato revocato.
Un database CRL centrale offre caratteristiche interessanti. La revoca diventa quasi
istantanea. Una volta che un certificato è stato aggiunto al CRL, non vengono più autorizzate altre transazioni. La revoca è molto affidabile, e non c’è un limite al numero di
certificati che possono essere revocati.
Questo database, però, ha anche notevoli svantaggi. Per poter consultare il database CRL
è necessario essere connessi online. Il database inoltre introduce un punto di potenziale
fallimento: se non è disponibile, nessuna azione può essere eseguita. Se si tenta di risol-

Le PKI nella realtà   263

vere questo problema autorizzando le parti a procedere ogni volta che il CRL non è
disponibile, gli attaccanti utilizzeranno attacchi di tipo denial-of-service per disabilitare
il database CRL ed eliminare la capacità di revoca del sistema.
Un’alternativa è quella di utilizzare un database CRL distribuito. Si potrebbe creare un
database ridondante con mirror disseminati in una decina di server in tutto il mondo, e
sperare che sia sufficientemente affidabile. Ma tutti quei database ridondanti sono costosi da realizzare e da gestire, e normalmente questa non è una strada praticabile. Non
dimenticate che raramente le persone sono disposte a investire denaro nella sicurezza.
Alcuni sistemi si limitano a inviare copie dell’intero database CRL a ogni dispositivo.
Il telefono cifrato STU-III dell’esercito statunitense funziona in questo modo. È una
soluzione simile a quella degli elenchi contenenti numeri di carte di credito rubati che
venivano inviati a tutti i commercianti. È una strada relativamente facile: basta che ogni
dispositivo prelevi il CRL aggiornato da un server web ogni mezzora, più o meno, con
il costo di un allungamento del tempo richiesto per la revoca. Tuttavia, questa soluzione limita la dimensione del database CRL. Nella maggior parte dei casi non ci si può
permettere di copiar centinaia di migliaia di voci CRL su ogni dispositivo del sistema.
Abbiamo incontrato sistemi in cui i requisiti stabiliscono che ogni dispositivo deve essere
in grado di memorizzare una lista di 50 voci CRL, e possono nascere dei problemi.
Secondo la nostra esperienza, i sistemi CRL sono costosi da implementare e da gestire.
Richiedono infrastruttura, management, percorsi di comunicazione e così via. Servono
numerose funzionalità aggiuntive soltanto per gestire la funzionalità della revoca, che è
utilizzata relativamente di rado.

19.8.2 Scadenza rapida
Invece degli elenchi di revoche, si può utilizzare il metodo della scadenza rapida, che sfrutta
il meccanismo di scadenza già esistente. La CA emette certificati con tempo di scadenza
molto ridotto, da 10 minuti a 24 ore. Ogni volta che Alice vuole utilizzare il proprio
certificato, ne chiede uno nuovo alla CA, che può utilizzare finché rimane valido. La
scadenza esatta può essere regolata in base ai requisiti dell’applicazione, ma un periodo
di validità inferiore a 10 minuti non sembra molto utile.
Il principale vantaggio di questo schema è che utilizza il meccanismo di emissione di
certificati già esistente, senza la necessità di un CRL separato, e questo riduce notevolmente la complessità del sistema.Tutto ciò che occorre fare per revocare un permesso è
informare la CA delle nuove regole di accesso. Naturalmente è necessario che l’utente
sia connesso per ottenere la nuova emissione del certificato.
Per noi la semplicità è uno dei principali criteri di progettazione, perciò preferiamo
utilizzare la scadenza rapida anziché un database CRL. La possibilità di utilizzare questa soluzione dipende principalmente dal fatto che l’applicazione richieda una revoca
istantanea o accetti un ritardo.

19.8.3 Verifica di certificati online
Un’altra alternativa è la verifica di certificati online. Questo approccio, accorpato nel protocollo OCSP (Online Certificate Status Protocol), ha riscosso un notevole successo in alcuni
settori, come quello dei browser web.

264   Capitolo 19

Per verificare un certificato, Alice interroga una parte fidata, quale la CA o un delegato,
inviando il numero di serie del certificato in questione. L’autorità cerca nel proprio
database lo stato del certificato e fornisce ad Alice una risposta firmata. Alice conosce la
chiave pubblica dell’autorità e può verificare la firma sulla risposta: se questa indica che
il certificato è valido, Alice sa che il certificato non è stato revocato.
La verifica di certificati online presenta molti vantaggi. Come nel caso dei CRL, la revoca
è quasi istantanea, e inoltre risulta molto affidabile. Questo meccanismo condivide anche
alcuni svantaggi dei CRL: Alice deve essere online per poter verificare un certificato, e
la parte fidata diventa un punto di possibile fallimento.
In generale preferiamo la verifica di certificati online all’uso dei CRL, perché evita il
problema di distribuire in modo massivo i CRL e la necessità di analizzarli e verificarli
sul client. Di conseguenza, i protocolli per la verifica di certificati online possono essere
progettati in modo più pulito, semplice e scalabile rispetto al meccanismo dei CRL.
Nella maggior parte dei casi, tuttavia, la verifica di certificati online è inferiore al meccanismo della scadenza rapida. Con la verifica online non è possibile verificare la chiave
senza la firma di una parte fidata. Se si considera tale firma come un nuovo certificato
per la chiave, si ottiene un sistema a scadenza rapida con periodi di scadenza molto ridotti. Lo svantaggio della verifica di certificati online è che per ogni verifica è necessario
interrogare la parte fidata, mentre nel caso della scadenza rapida si può utilizzare la stessa
firma della CA per molte verifiche.

19.8.4 Revoca obbligatoria
Poiché l’implementazione della revoca può risultare difficile, c’è la forte tentazione di
non effettuarla. Alcune proposte di PKI non fanno menzione della revoca, mentre altre
citano i CRL come futura possibilità di estensione. Nella realtà, una PKI senza qualche
forma di revoca è praticamente inutile. Le circostanze della vita reale fanno sì che talvolta
le chiavi vengano effettivamente compromesse, e quindi che capiti di dover revocare
l’accesso. Utilizzare una PKI senza un sistema di revoca funzionante è come condurre
una barca senza una pompa di sentina. In teoria la barca dovrebbe essere stagna e non
richiedere una pompa di sentina, ma nella pratica c’è sempre dell’acqua che si raccoglie
nella parte inferiore dello scafo, e se non la si rimuove, la barca finirà per affondare.

19.9 In conclusione, a che cosa serve una PKI?
All’inizio della discussione sulle PKI abbiamo affermato che lo scopo di utilizzare una
PKI è quello di consentire ad Alice e Bob di generare una chiave segreta condivisa, che
utilizzano per creare un canale sicuro, che utilizzano per comunicare tra loro in modo
sicuro. Alice vuole autenticare Bob (e viceversa) senza comunicare con una terza parte,
e la PKI dovrebbe rendere possibile ciò.
Ma non funziona.
Non esiste alcun sistema di revoca che funziona interamente offline. È facile capire perché:
se né Alice né Bob contatta una parte esterna, nessuno dei due può essere informato che
una delle sue chiavi è stata revocata. Perciò la revoca li costringe a connettersi online,
come abbiamo visto nelle soluzioni presentate precedentemente.

Le PKI nella realtà   265

Ma se siamo online, non abbiamo bisogno di una grande e complessa PKI. Possiamo
raggiungere il nostro livello di sicurezza desiderato semplicemente impostando un server
di chiavi centrale, come quelli descritti nel Capitolo 17.
Ora mettiamo a confronto i vantaggi di una PKI rispetto a un sistema con server di chiavi.
• Un server di chiavi richiede che ognuno sia connesso in tempo reale. Se non si è
in grado di raggiungere il server di chiavi, non si può fare nulla. Alice e Bob non
possono riconoscersi a vicenda. Una PKI fornisce alcuni vantaggi. Se si utilizza il
meccanismo di scadenza per la revoca, è sufficiente contattare il server centrale di
tanto in tanto; per applicazioni che utilizzano certificati con periodi di validità di ore,
il requisito di accesso ed elaborazione online in tempo reale è notevolmente allentato.
Questo è utile per applicazioni non interattive come l’email, e anche per certi sistemi
di autorizzazione, o quando le comunicazioni sono costose. Anche se si utilizza un
database CRL si potrebbero avere delle regole che indichino come procedere se
tale database è irraggiungibile. Nei sistemi di gestione delle carte di credito ci sono
regole di questo tipo: se non si riesce a ottenere un’autorizzazione automatica, tutte
le transazioni inferiori a una certa somma sono ammesse. Queste regole dovrebbero
essere basate su un’attenta analisi del rischio, che comprenda il rischio di un attacco
di tipo denial-of-service al sistema CRL, ma almeno si ha la possibilità di continuare;
la soluzione con il server di chiavi non fornisce alternative.
• Il server di chiavi è un singolo punto di potenziale fallimento. Distribuirlo è difficile,
perché contiene tutte le chiavi del sistema, e nessuno vuole diffondere le proprie
chiavi segrete dappertutto. Il database CRL, invece, è molto meno problematico dal
punto di vista della sicurezza e più facile da distribuire. La soluzione con scadenza
rapida fa della CA un punto di fallimento. Tuttavia, nei sistemi grandi c’è quasi
sempre una CA di tipo gerarchico, quindi già distribuita, e gli eventuali problemi
interessano soltanto una piccola parte del sistema.
• In teoria una PKI dovrebbe fornire un meccanismo di non ripudio: una volta che Alice
ha firmato un messaggio con la propria chiave, non dovrebbe più avere la possibilità
di ripudiare la propria firma. Un sistema con server di chiavi non può mai fornire
questo meccanismo: il server centrale ha accesso alla stessa chiave utilizzata da Alice e
quindi può falsificare un messaggio arbitrario facendo sembrare che lo abbia inviato
Alice. Nella vita reale il meccanismo di non ripudio non funziona, perché le persone
non sono in grado di memorizzare le proprie chiavi segrete in modo sufficientemente
sicuro. Se Alice vuole negare di aver firmato un messaggio, le basta affermare che un
virus ha infettato il suo computer e ha rubato la sua chiave privata.
• La chiave più importante di una PKI è la chiave di root della CA, che non deve
essere memorizzata in un computer online, ma può essere archiviata in modo sicuro
e caricata in un computer online soltanto quando serve. La chiave di root è utilizzata
soltanto per firmare certificati delle sotto-CA, cosa che si fa raramente. Il sistema
con server di chiavi, invece, memorizza la chiave master in un computer online. I
computer offline sono molto meno esposti agli attacchi di quelli online, perciò questa
caratteristica rende una PKI potenzialmente più sicura.
Le PKI hanno alcuni vantaggi, ma nessuno di essi risulta davvero fondamentale in alcuni
ambienti. Inoltre, questi vantaggi si ottengono a un prezzo elevato: una PKI è molto più
complessa di un sistema con server di chiavi, e i calcoli a chiave pubblica richiedono
parecchia potenza di calcolo in più.

266   Capitolo 19

19.10 Che cosa scegliere
A questo punto vi chiederete quale sia il modo migliore per configurare un sistema di
gestione delle chiavi. È meglio utilizzare un meccanismo con server di chiavi, o con
PKI? Come sempre, dipende dai requisiti, dalla dimensione del sistema, dall’applicazione
e così via. Per sistemi piccoli, in generale non vale la pena di utilizzare una PKI, troppo
complessa. Riteniamo più semplice utilizzare il server di chiavi. Il fatto è che i vantaggi
di una PKI rispetto all’approccio con server di chiavi sono più significativi per sistemi
di grandi dimensioni.
Per sistemi grandi, la flessibilità aggiuntiva di una PKI è un bel vantaggio. Una PKI può
essere un sistema maggiormente distribuito. Le estensioni con credenziali consentono
alla CA centrale di limitare l’autorità delle sotto-CA. Ciò a sua volta facilita l’impostazione di piccole sotto-CA che coprono particolari aree di attività. Poiché la sotto-CA
può emettere certificati solo rispettando i limiti specificati dal certificato della propria
chiave, non può costituire un rischio per il sistema nel suo complesso. Nei sistemi grandi,
flessibilità e limitazione del rischio sono aspetti importanti.
Se state costruendo un sistema grande, vi consigliamo di esaminare molto seriamente
una soluzione con PKI, ma comunque di metterla a confronto con una soluzione basata
su server di chiavi. Dovete verificare se i vantaggi della PKI compensano i costi e la
complessità aggiuntivi. Un problema potrebbe sorgere se volete veramente utilizzare le
limitazioni tipo credenziali per le vostre sotto-CA. Per fare ciò, dovete essere in grado
di esprimere tali limitazioni in una struttura logica, ma non esiste una struttura generica
in cui è possibile farlo, perciò questa parte del progetto finisce per risultare specifica
del cliente. Inoltre, questo probabilmente vi impedirà di utilizzare una PKI già pronta,
perché questi prodotti difficilmente dispongono di un linguaggio appropriato per i
vincoli sui certificati.

19.11 Esercizi
Esercizio 19.1 Che problemi potrebbero verificarsi se Alice utilizza le stesse chiavi
con più PKI?
Esercizio 19.2 Supponete che un sistema utilizzi dispositivi in grado di memorizzare
un elenco di 50 voci di CRL. In che modo questa scelta di progettazione può comportare
problemi di sicurezza?
Esercizio 19.3 Supponete che un sistema utilizzi una PKI con un CRL. Un dispositivo del sistema riceve la richiesta di verificare un certificato, ma non può accedere al
database CRL a causa di un attacco di tipo denial-of-service. Che linee di azione può
seguire il dispositivo, e quali sono vantaggi e svantaggi di ciascuna?
Esercizio 19.4 Mettete a confronto vantaggi e svantaggi di PKI e server di chiavi.
Descrivete un’applicazione di esempio per cui utilizzereste una PKI, e una per cui utilizzereste un server di chiavi. Spiegate le vostre decisioni.
Esercizio 19.5 Mettete a confronto vantaggi e svantaggi di CRL, scadenza rapida e
verifica di certificati online. Descrivete un’applicazione di esempio per cui utilizzereste
un CRL, una per cui utilizzereste la scadenza rapida e una per cui utilizzereste la verifica
di certificati online. Spiegate le vostre decisioni.

Capitolo 20

Considerazioni pratiche
sulle PKI
Nella pratica, se avete bisogno di una PKI, dovrete decidere se acquistarla o costruirla. Nel seguito
esaminiamo alcune delle considerazioni pratiche da
svolgere quando si progetta un sistema PKI.

Sommario
20.1 Il formato del
certificato
20.2 La vita di una chiave

20.1 Il formato del certificato

20.3 Perché le chiavi si
usurano

Un certificato è semplicemente un tipo di dati con
più campi obbligatori e facoltativi. È importante che
la codifica di una particolare struttura dati sia unica,
perché in crittografia spesso si sottopone ad hash una
struttura dati per firmarla o confrontarla. Un formato
come XML, che consente diverse rappresentazioni
della stessa struttura dati, richiede ancora più attenzione per assicurarsi che firme e hash funzionino
sempre come dovrebbero. I certificati X.509, anche
se troppo complessi per i nostri gusti, costituiscono
un’altra alternativa.

20.4 Il cammino prosegue

20.1.1 Il linguaggio per i permessi
Quando si utilizza un sistema PKI, eccetto quelli
più semplici, si vuole avere la possibilità di limitare i
certificati che una sotto-CA può emettere. A scopo
è necessario codificare un vincolo nel certificato
della sotto-CA, e per fare ciò occorre un linguaggio
in cui esprimere i permessi della chiave. Questo è
probabilmente l’aspetto più arduo della progettazione di PKI. Le limitazioni necessarie dipendono
dall’applicazione. Se non riuscite a trovare limitazioni
sensate, dovreste riconsiderare la scelta di utilizzare

20.5 Esercizi

268   Capitolo 20

una PKI. Senza limitazioni nei certificati, infatti, ogni sotto-CA in sostanza ha una chiave
master, e questo è un male per la sicurezza. Potreste limitarvi a un’unica CA, ma allora
perdereste molti dei vantaggi delle PKI rispetto ai server di chiavi.

20.1.2 La chiave di root
Per poter lavorare, la CA deve avere una coppia di chiavi pubblica/privata. Generare
questa coppia di chiavi è semplice. La chiave pubblica deve essere distribuita a ogni utente,
con alcuni dati extra quali il suo periodo di validità. Per semplificare il sistema, questo
normalmente viene fatto utilizzando un certificato autocertificante, entità piuttosto strana.
La CA firma un certificato per la propria chiave pubblica. Il termine “autocertificante” è
fuorviante, è un residuo del passato rimasto in uso ancora oggi. In realtà il certificato non
certifica affatto la chiave, e non prova nulla sulle caratteristiche di sicurezza della chiave,
perché chiunque può creare una chiave pubblica e autocertificarla. Questo certificato
serve ad associare dati aggiuntivi alla chiave pubblica. Elenco di permessi, periodo di
validità, dati di contatto personale e altro sono inclusi nel certificato autocertificante.
Tale certificato utilizza lo stesso formato di tutti gli altri certificati del sistema e tutti
gli utenti possono riutilizzare il codice esistente per controllare questi dati aggiuntivi.
Questo certificato particolare è chiamato certificato di root della PKI.
A questo punto occorre distribuire il certificato di root a tutti gli utenti del sistema, in modo
sicuro. Ogni utente deve conoscere il certificato di root, e deve avere quello giusto.
La prima volta che un computer entra nella PKI, dovrà ricevere il certificato di root
trasmesso in modo sicuro. Basta indirizzare il computer su un file locale o residente su un
server web fidato, indicando alla macchina che si tratta del certificato di root per la PKI
in questione. In questa distribuzione iniziale del certificato di root la crittografia non può
fornire alcun aiuto, perché non ci sono chiavi utilizzabili per fornire l’autenticazione. La
stessa situazione si verifica se la chiave privata della CA viene compromessa. Quando la
chiave di root non è più sicura, si dovrà inizializzare una struttura PKI del tutto nuova, e
quindi inviare a ogni utente il certificato di root, con una trasmissione sicura. Tutto ciò
è un buon motivo per mantenere ben protetta la chiave di root.
Dopo un certo periodo la chiave di root scade, e la CA centrale dovrà emetterne una
nuova. Distribuire il nuovo certificato di root è più facile, perché può essere firmato
con la vecchia chiave di root. Gli utenti possono prelevare il nuovo certificato di root
da una fonte non sicura; poiché è formato con la vecchia chiave di root, il certificato
non può essere modificato. L’unico possibile problema si ha se un utente non ottiene il
nuovo certificato di root. Nella maggior parte dei sistemi i periodi di validità delle chiavi
di root sono impostati in modo da creare una certa sovrapposizione, per dare il tempo
necessario al cambio della chiave.
C’è un piccolo problema di implementazione. Il nuovo certificato di root probabilmente
dovrà avere due firme: una apposta con la vecchia chiave di root, in modo che gli utenti
possano riconoscerlo, e una (autocertificante) con la nuova chiave di root, utilizzabile
dai nuovi dispositivi introdotti dopo la scadenza della vecchia chiave. Per fare ciò si può
includere il supporto di firme multiple nel formato del certificato, o semplicemente
emettere due certificati separati per la nuova chiave di root.

Considerazioni pratiche sulle PKI   269

20.2 La vita di una chiave
Consideriamo la durata di una singola chiave, che può essere la chiave di root della CA
o qualsiasi altra chiave pubblica. Un chiave attraversa diverse fasi durante la propria vita,
descritte di seguito; non è detto che le attraversi tutte, dipende dall’applicazione. Come
esempio faremo riferimento alla chiave pubblica di Alice.
• Creazione. La prima fase della vita di una chiave è la creazione. Alice crea una
coppia di chiavi pubblica/privata e memorizza la parte privata in modo sicuro.
• Certificazione. La fase successiva è la certificazione. Alice invia la propria chiave
pubblica alla CA o a una sotto-CA chiedendo di certificarla. Questo è il punto in
cui la CA decide quali permessi assegnare alla chiave pubblica di Alice.
• Distribuzione. A seconda dell’applicazione, Alice potrebbe dover distribuire la
propria chiave pubblica certificata, prima di poterla usare. Per esempio, se Alice
utilizza la propria chiave per delle firme, ogni parte che potrebbe ricevere una firma di Alice deve disporre prima della sua chiave pubblica. La soluzione migliore è
quella di distribuire la chiave prima che Alice la utilizzi per la prima volta. Questo
è particolarmente importante per un nuovo certificato di root. Quando la CA passa
a una nuova chiave di root, per esempio, tutti dovrebbero avere la possibilità di conoscere il nuovo certificato di root prima di ricevere un certificato firmato con la
nuova chiave. La necessità di una fase di distribuzione separata dipende dall’applicazione. Se potete evitarla, fatelo. Questa fase va spiegata agli utenti e diviene visibile
nell’interfaccia utente. Questo fatto a sua volta genera parecchio lavoro aggiuntivo,
perché molti utenti non capiranno che cosa succede e non utilizzeranno il sistema
nel modo corretto.
• Uso attivo. La fase successiva è quella in cui Alice utilizza attivamente la propria
chiave per le transazioni. Questa è la situazione normale per una chiave.
• Uso passivo. Dopo la fase di uso attivo deve esserci un periodo di tempo in cui
Alice non usa più la chiave per nuove transazioni, ma tutti continuano ad accettarla.
Le transazioni non sono istantanee, a volte vengono ritardate. Un’email firmata potrebbe anche richiedere un giorno o due per arrivare a destinazione. Alice dovrebbe
cessare di utilizzare attivamente la propria chiave e lasciar passare un periodo di tempo
ragionevole per fare in modo che tutte le transazioni pendenti siano completate
prima della scadenza della chiave.
• Scadenza. Alla fine la chiave scade e non è considerata più valida.
Come si definiscono le fasi della chiave? La soluzione più comune è quella di inserire nel
certificato i tempi espliciti per ciascuna transizione di fase. Il certificato contiene l’inizio
della fase di distribuzione, l’inizio della fase di uso attivo e la scadenza. Sfortunatamente
tutti questi tempi devono essere mostrati all’utente, perché influiscono sul funzionamento
del certificato, e probabilmente è tutto troppo complicato per i normali utenti.
Uno schema più flessibile prevede l’utilizzo di un database centrale che contiene la fase
di ciascuna chiave, ma così si introduce una nuova serie di problemi di sicurezza. Se si
utilizza un CRL, può ridefinire i periodi di fase scelti e far scadere immediatamente
una chiave.
Le cose si fanno ancora più complicate se Alice vuole utilizzare la stessa chiave in PKI
diverse. In generale la riteniamo una cattiva idea, ma a volte non si può evitare.Tuttavia,

270   Capitolo 20

è necessario prendere delle precauzioni in più. Supponete che Alice utilizzi un piccolo
modulo resistente alle manomissioni che porta sempre con sé. Questo modulo contiene le
sue chiavi private e svolge i calcoli necessari per una firma digitale. Simili moduli hanno
capacità di memorizzazione limitata. I certificati della chiave pubblica di Alice possono
essere registrati sulla intranet aziendale senza limitazioni di dimensione, ma il piccolo
modulo non può contenere un numero illimitato di chiavi private. In situazioni come
queste,Alice finirà per utilizzare la stessa chiave per più PKI. Questo implica anche che la
durata della chiave dovrebbe essere simile per tutte le PKI utilizzate da Alice. Coordinare
il tutto potrebbe risultare difficile. Se vi capiterà di lavorare su un sistema come questo,
assicuratevi che una firma utilizzata in una PKI non possa essere utilizzata in un’altra PKI.
Dovreste sempre utilizzare un unico schema di firma digitale, come quello descritto nel
Paragrafo 12.7. La stringa di byte firmata non deve mai essere la stessa in due PKI diverse
o in due applicazioni diverse. La soluzione più semplice è quella di inserire nella stringa
da firmare dei dati che identifichino univocamente l’applicazione e la PKI.

20.3 Perché le chiavi si usurano
Abbiamo detto più volte che le chiavi devono essere sostituite a intervalli regolari, ma
perché?
In un mondo perfetto, una chiave potrebbe essere utilizzata per un periodo molto lungo.
Un attaccante che non abbia punti deboli su cui lavorare sarà costretto a eseguire ricerche
esaustive. In teoria questo riduce il nostro problema alla scelta di chiavi sufficientemente
grandi.
Ma il mondo reale non è perfetto e la segretezza di una chiave è sempre sotto minaccia.
La chiave deve essere memorizzata da qualche parte, e un attaccante potrebbe cercare
di arrivarci. Inoltre la chiave deve essere usata, e qualsiasi uso pone un’altra minaccia. La
chiave deve essere trasportata dalla posizione in cui è memorizzata al punto in cui sono
svolti i calcoli. Spesso si rimane all’interno di un unico dispositivo, ma anche così si apre
una nuova via di attacco. Se l’attaccante riesce a intercettare il canale di comunicazione utilizzato per il trasporto, può ottenere una copia della chiave. Poi c’è l’operazione
crittografica svolta con la chiave. Non esistono funzioni crittografiche utili che possano
essere considerate assolutamente sicure. Le prove di sicurezza in fondo si basano tutte
su argomenti del tipo: “Be’, nessuno di noi ha trovato un modo per attaccare questa
funzione, quindi sembra piuttosto sicura” (le cosiddette “dimostrazioni di sicurezza” per
funzioni crittografiche in realtà non sono dimostrazioni complete, ma in genere semplici
riduzioni: se si può violare la funzione A, si può violare anche B. Queste procedure sono
utili per ridurre il numero di operazioni primitive che possono essere considerate sicure,
ma non forniscono una dimostrazione completa di sicurezza). E come abbiamo già visto,
i canali laterali possono far trapelare informazioni sulle chiavi.
Più tempo si mantiene una chiave, e più la si utilizza, più aumenta la probabilità che un
attaccante possa riuscire a impadronirsene. Se volete limitare la probabilità che l’attaccante arrivi a conoscere la vostra chiave, dovete limitare la sua durata. In effetti, le chiavi
si usurano con il tempo.
Esiste un altro motivo per limitare la durata di una chiave. Supponete che qualcosa vada
storto e l’attaccante riesca a ottenere la chiave. Questo compromette la sicurezza del
sistema e causa danni di vario tipo (la revoca è efficace soltanto se venite a sapere che

Considerazioni pratiche sulle PKI   271

l’attaccante ha ottenuto la chiave; un attaccante astuto cerca di non farsi scoprire). Il danno
permane finché la chiave viene sostituita da una nuova, e anche allora, i dati cifrati con
la vecchia chiave rimangono compromessi. Limitando la durata di una singola chiave, si
limita la finestra di esposizione a un attaccante che sia riuscito a procurarsela.
Impostare una durata breve dalla chiave porta un duplice vantaggio: si riducono le possibilità che un attaccante ottenga la chiave e si limitano i danni che si verificano qualora
l’attaccante riesca nel suo scopo.
Ma qual è una durata ragionevole? Dipende dalla situazione. Sostituire la chiave ha un
costo, perciò non è il caso di farlo troppo spesso. D’altra parte, se si sostituiscono le chiavi
una volta ogni dieci anni, non si può essere certi che dopo un decennio la funzione di
sostituzione funzionerà. Come regola generale, una funzione o procedura utilizzata o
testata di rado ha maggiori probabilità di fallire (questo vale in generale ed è il motivo
principale per cui si dovrebbero sempre testare le procedure di emergenza, come le
esercitazioni antincendio).
Probabilmente il maggior pericolo di avere chiavi di lungo termine è che la funzione
di sostituzione della chiave non viene mai utilizzata, e quindi c’è il rischio che non funzioni bene ove serva. Una durata di un anno è probabilmente il massimo da considerare
ragionevole.
Le procedure di sostituzione della chiave in cui l’utente deve essere coinvolto sono relativamente costose, perciò dovrebbero essere svolte raramente. I periodi ragionevoli per
la durata della chiave vanno da un mese in su. Con durate più brevi le chiavi dovranno
essere gestite in modo automatico.

20.4 Il cammino prosegue
La gestione delle chiavi non è solo un problema di crittografia, ma riguarda l’interfaccia
con il mondo reale. La scelta specifica di quale PKI utilizzare, e di come configurarla,
dipende dalle caratteristiche specifiche dell’applicazione e dell’ambiente in cui dovrà
essere messa in opera. Abbiamo delineato gli aspetti fondamentali da considerare.

20.5 Esercizi
Esercizio 20.1 Quali campi dovrebbero essere riportati in un certificato, e perché?
Esercizio 20.2 Quali sono le chiavi SSL di root codificate nel vostro browser web?
Quando sono state create? Quando scadono?
Esercizio 20.3 Supponete di aver installato una PKI, e che tale PKI utilizzi i certificati in un determinato formato fisso. Dovete aggiornare il vostro sistema, mantenendo
la compatibilità con la versione originale della PKI e dei suoi certificati. Ma il sistema
aggiornato richiede certificati contenenti campi aggiuntivi. Quali problemi potrebbero
sorgere in questa transizione? Quali passi avreste potuto intraprendere al momento della
progettazione del sistema, per predisporre al meglio un’eventuale transizione a un nuovo
formato dei certificati?

272   Capitolo 20

Esercizio 20.4 Create un certificato autofirmato utilizzando i pacchetti o le librerie
crittografiche disponibili sulla vostra macchina.
Esercizio 20.5 Trovate un nuovo prodotto o sistema che utilizzi una PKI. Potrebbe
essere lo stesso prodotto o sistema che avete analizzato per l’Esercizio 1.8. Effettuate una
revisione della sicurezza di tale prodotto o sistema secondo le indicazioni del Paragrafo
1.12, ma questa volta concentrandovi sugli aspetti di sicurezza e riservatezza legati all’uso
della PKI.

Capitolo 21

Come conservare
i segreti
Abbiamo discusso il problema di memorizzare segreti
transitori, come le chiavi di sessione, nel Paragrafo
8.3. Ora ci chiediamo come memorizzare segreti
a lungo termine, come password e chiavi private.
Abbiamo due requisiti di tipo opposto: primo, il segreto va tenuto segreto; secondo, il rischio di perdere
irrimediabilmente il segreto (cioè di non poterlo più
trovare) deve essere ridotto al minimo.

21.1 Unità a disco
L’idea più banale è quella di memorizzare il segreto
sul disco fisso del computer, o su qualche altro supporto di memorizzazione permanente. Funziona, ma
solo se il computer è mantenuto al sicuro. Se Alice
salva le proprie chiavi (senza cifratura) sul proprio
PC, chiunque abbia accesso al suo PC può utilizzarle. La maggior parte dei PC è utilizzata anche
da altre persone, almeno occasionalmente. Alice è
disponibile a lasciare che altri utilizzino il suo PC,
ma certamente non vuole concedere a tutti l’accesso
al suo conto corrente. Un altro problema è che Alice
probabilmente utilizza più computer. Se le sue chiavi
sono memorizzate nel suo PC di casa, non potrà
utilizzarle al lavoro o quando è in viaggio. Inoltre,
dovrebbe memorizzarle sul computer desktop di casa
o sul portatile? Non è il caso di copiare le chiavi in
troppi punti, perché così si indebolisce la sicurezza
del sistema.
Una soluzione migliore per Alice sarebbe quella di
memorizzare le chiavi sul proprio smartphone o PDA.
Capita meno frequentemente di prestare ad altri que-

Sommario
21.1 Unità a disco
21.2 Memoria umana
21.3 Supporti di
archiviazione portatili
21.4 Dispositivi sicuri
21.5 Interfaccia utente
sicura
21.6 Biometria
21.7 Sistemi SSO (Single
Sign-On)
21.8 Rischio di perdita
dei dati
21.9 Secret sharing
21.10 Cancellazione dei
segreti
21.11 Esercizi

274   Capitolo 21

sti apparecchi, che in genere si portano sempre con sé.Tuttavia, piccoli apparecchi come
questi si possono facilmente perdere o rubare, e non vogliamo che qualcuno entrato in
possesso dell’apparecchio possa accedere alle chiavi segrete.
Probabilmente pensate che la sicurezza migliorerebbe se si eseguisse la cifratura dei segreti.
Certamente, ma con che cosa? Serve una chiave master con cui cifrare i segreti, e tale
chiave va anch’essa memorizzata da qualche parte. Se la si conserva vicino ai segreti cifrati,
non se ne trae alcun alcun vantaggio. Tuttavia, questa è una buona tecnica per ridurre
il numero e la dimensione dei segreti, ed è ampiamente utilizzata in combinazione con
altre. Per esempio, una chiave privata RSA è lunga diverse migliaia di bit, ma cifrandola
e autenticandola con una chiave simmetrica possiamo ridurre di un fattore significativo
la dimensione dello spazio richiesto per memorizzarla.

21.2 Memoria umana
La seconda idea è quella di memorizzare la chiave nel cervello di Alice. Le indichiamo di
memorizzare una password e di cifrare con essa tutti gli altri dati della chiave. Il materiale
cifrato può essere conservato ovunque, per esempio su disco, ma anche su un server web
da cui Alice può prelevarlo su qualsiasi computer utilizzi al momento.
È noto che gli esseri umani non sono bravi a memorizzare le password. Se scegliete
password molto semplici, non ottenete alcuna sicurezza. Il fatto è che le password semplici non sono in numero sufficiente per poter essere davvero segrete: l’attaccante può
semplicemente provarle tutte. Utilizzare il cognome da ragazza di vostra madre non
funziona molto bene; spesso è un cognome conosciuto a molti, e anche se non lo è,
probabilmente esistono poche centinaia di migliaia di nomi che l’attaccante dovrebbe
provare prima di trovare quello giusto.
Una buona password deve essere impossibile da prevedere. In altre parole, deve contenere
molta entropia. Le parole normali, come le password, non contengono entropia sufficiente.
La lingua inglese contiene circa mezzo milione di parole (contando anche quelle più
lunghe e oscure che si trovano solo nei dizionari integrali), quindi una singola parola
fornisce al massimo 19 bit di entropia. Le stime della quantità di entropia per carattere
di testo inglese variano leggermente, ma si aggirano attorno a 1,5–2 bit per lettera.
Abbiamo utilizzato chiavi segrete di 256 bit in tutti i nostri sistemi per raggiungere 128
bit di sicurezza. Nella maggior parte dei casi l’utilizzo di una chiave di 256 bit comporta
un costo aggiuntivo molto ridotto. Tuttavia, in questo scenario l’utente deve imparare
a memoria la password (o la chiave), e il costo aggiuntivo di chiavi molto lunghe è alto.
Utilizzare password con 256 bit di entropia è troppo pesante, perciò ci limiteremo a
password con 128 bit di entropia (per i matematici: password scelte da una distribuzione
di probabilità con 128 bit di entropia).
Con la stima ottimistica di 2 bit per carattere, ci servirebbe una password di 64 caratteri
per ottenere 128 bit di entropia. Non è accettabile: gli utenti si rifiuteranno di utilizzare
password così lunghe.
E se facciamo un compromesso e accettiamo 64 bit di sicurezza? La situazione non migliora di molto. Con 2 bit di entropia per carattere, ci serve una password di almeno 32
caratteri. È ancora troppo lunga; non dimenticate che la maggior parte delle password
nel mondo reale è lunga da 6 a 8 caratteri.

Come conservare i segreti    275

Potreste tentare di ricorrere a password assegnate, ma avete mai provato a utilizzare un
sistema in cui vi si dice che la vostra password è “7193275827429946905186”? O “aoekjk3ncmakwe”? Gli umani non sono in grado di tenere a mente password come queste,
perciò questa soluzione non funziona (nella pratica gli utenti scriveranno la password da
qualche parte, ma discuteremo questo punto nel paragrafo seguente).
Una soluzione migliore è quella di utilizzare una passphrase. Una passphrase è simile a
una password, tanto che le consideriamo equivalenti. La distinzione dei termini serve
solo a indicare una maggiore lunghezza nel caso della passphrase.
Forse Alice potrebbe utilizzare la passphrase “Tende rosanero si aggirano nell’oceano”.
Non ha alcun senso, ma è abbastanza semplice da ricordare. È lunga 38 caratteri, perciò probabilmente contiene circa 57–76 bit di entropia. Se Alice la espande in “Tende
rosanero si aggirano nei mari vicino al Natale” ottiene 52 caratteri, per una chiave assai
ragionevole con 78–104 bit di entropia. Con una tastiera a disposizione Alice è in grado
di digitare questa passphrase in pochi secondi, in un tempo molto inferiore rispetto a
quello che richiederebbe una stringa di cifre casuali. Sfruttiamo il fatto che una passphrase è molto più facile da memorizzare che un insieme di dati casuali. Molte tecniche
mnemoniche si basano sull’idea di convertire dati casuali in elementi per molti aspetti
simili alle nostre passphrase.
Alcuni utenti non amano digitare frasi troppo lunghe, perciò scelgono le proprie passphrase in un modo leggermente diverso. Considerate la passphrase “NmdcdnvmrpusocldvesAqadqeècdesseaefcnprlp”. Sembra del tutto priva di senso, ma pensatela come la
sequenza delle prime lettere delle parole di una frase; in questo caso abbiamo utilizzato
i famosi versi di Dante: “Nel mezzo del cammin di nostra vita/mi ritrovai per una selva
oscura/che la diritta via era smarrita./Ahi quanto a dir qual era è cosa dura/esta selva
selvaggia e aspra e forte/che nel pensier rinova la paura!”. Naturalmente Alice non dovrebbe utilizzare un testo letterario, perché i testi letterari sono facilmente accessibili a
un attaccante, e occorrerebbe chiedersi quante frasi adatte si troverebbero nei libri della
sua biblioteca. Dovrebbe invece utilizzare una frase inventata da lei stessa, che nessun
altro possa indovinare.
Rispetto all’utilizzo di una passphrase completa, la tecnica delle lettere iniziali di ogni
parola richiede un testo più lungo, ma consente di digitare meno caratteri per ottenere
una buona sicurezza, perché comporta maggiore casualità nei caratteri inseriti, rispetto a
quando si utilizzano lettere consecutive di una frase. Non conosciamo stima del numero
di bit di entropia per carattere ottenuto con questa tecnica.
Le passphrase rappresentano certamente il modo migliore per conservare un segreto
in una mente umana. Sfortunatamente, però, molti utenti hanno difficoltà a utilizzarle
correttamente. Anche con le passphrase, inoltre, è estremamente difficile arrivare a 128
bit di entropia nella mente umana.

21.2.1 Sale e allungamento
Per spremere la massima sicurezza possibile da una password o passphrase di entropia
limitata, possiamo utilizzare due operazioni che sembrerebbero provenire da una stanza
delle torture medievale. Sono tanto semplici e ovvie che dovrebbero essere utilizzate in
ogni sistema di password.

276   Capitolo 21

La prima operazione consiste nell’introdurre un sale (salt in inglese): ovvero un numero
casuale memorizzato assieme ai dati che sono stati cifrati con la password. Se possibile,
utilizzate un sale di 256 bit.
Il passaggio successivo consiste nell’allungamento della password. Sia p la password e s il
sale. Utilizzando una qualsiasi funzione di hash crittograficamente forte h, calcoliamo:




x0 := 0
xi := h(xi−1 || p || s) per i = 1, ... , r
K := xr

e utilizziamo K come chiave per cifrare effettivamente i dati. Il parametro r è il numero
di iterazioni nel calcolo e dovrebbe essere il più grande possibile (non c’è bisogno di
dire che xi, e K dovrebbero avere lunghezza di 256 bit).
Esaminiamo la situazione dal punto di vista dell’attaccante. Dati il sale s e dei dati cifrati
con K, l’attaccante cerca di determinare K provando diverse password. Sceglie una particolare password p, calcola il K corrispondente, decifra i dati e verifica se hanno senso e
superano i controlli di integrità associati. Se la verifica non è superata, p non è la password
giusta. Per verificare un singolo valore per p, l’attaccante deve eseguire r diversi calcoli
di hash. Più grande è r, più lavoro deve fare.
Talvolta è utile essere in grado di verificare se la chiave ricavata è corretta prima di
decifrare i dati. In questo caso, si può calcolare un valore di controllo della chiave. Per
esempio, il valore di controllo della chiave potrebbe essere h(0 || xi−1 || p || s), che grazie
alle proprietà delle funzioni di hash è indipendente da K. Questo valore dovrebbe essere
memorizzato assieme al sale e potrebbe essere impiegato per controllare la password
prima di decifrare i dati con K.
Nell’uso normale, il calcolo dell’allungamento deve essere effettuato ogni volta che
si utilizza una password. Ricordate però che siamo in un momento in cui l’utente ha
appena digitato una password. Probabilmente l’inserimento della password ha richiesto
diversi secondi, perciò utilizzare 200 ms per l’elaborazione della password è accettabile.
Ecco la nostra regola per la scelta di r: scegliere r tale che il calcolo di K da (s, p) richieda 200-1000 ms sulla macchina dell’utente. I computer diventano sempre più veloci,
perciò anche r dovrebbe aumentare con il passare del tempo. Idealmente si dovrebbe
determinare r sperimentalmente quando l’utente imposta la password per la prima volta,
e registrarlo assieme a s (assicuratevi che r sia un valore ragionevole, non troppo piccolo
né troppo grande).
Quanto abbiamo guadagnato? Se r = 220 (poco più di un milione), l’attaccante deve
eseguire 220 calcoli di hash per ogni password che prova. Per provare 260 password dovrebbe eseguire 280 calcoli di hash. L’utilizzo di r = 220 allunga la dimensione effettiva
della password di 20 bit. Maggiore è r, maggiore è il guadagno ottenuto.
Esaminiamo le cose da un altro punto di vista. r serve a impedire che l’attaccante sfrutti
la sempre maggiore potenza dei computer, perché all’aumentare della velocità delle
macchine, aumenta anche il valore di r. È una sorta di compensatore della legge di Moore, ma solo nel lungo termine. Tra dieci anni l’attaccante potrà utilizzare la tecnologia
futura per attaccare le password che sono utilizzate oggi. Quindi rimane la necessità di
mantenere un buon margine di sicurezza e di ottenere la massima entropia possibile
dalla password.

Come conservare i segreti    277

Questo è un altro motivo per utilizzare un protocollo di negoziazione della chiave con
forward secrecy. Indipendentemente dall’applicazione, è piuttosto probabile che le chiavi
private di Alice finiranno per essere protette con una password.Tra dieci anni l’attaccante
sarà in grado di ricercare la password di Alice e di trovarla. Tuttavia, se la chiave cifrata
con la password è stata utilizzata soltanto per eseguire un protocollo di negoziazione della
chiave con forward secrecy, l’attaccante non troverà nulla di valore. La chiave di Alice
non sarà più valida (sarà scaduta) e conoscere la sua vecchia chiave privata non servirà a
scoprire le chiavi di sessione utilizzate dieci anni prima.
Il sale impedisce all’attaccante di sfruttare un’economia di scala quando attacca un gran
numero di password simultaneamente. Supponete che il sistema abbia un milione di
utenti e che ognuno conservi un file cifrato contenente le sue chiavi. Ogni file è cifrato
con la password allungata dell’utente. Se non utilizzassimo un sale, l’attaccante potrebbe
procedere nel modo seguente: provare una password p, calcolare la chiave allungata K e
con essa tentare di decifrare ognuno dei file di chiave. La funzione di allungamento deve
essere calcolata soltanto una volta per ogni password e la chiave allungata risultante può
essere utilizzata in un tentativo di decifrare ciascuno dei file.
Questo attacco non è più possibile se aggiungiamo un sale alla funzione di allungamento.
Tutti i sali sono valori casuali, perciò ogni utente ne utilizzerà uno diverso. L’attaccante
dovrà calcolare la funzione di allungamento una volta per ciascuna combinazione password/file, anziché una volta per ciascuna password. Si tratta di molto lavoro in più, e
questo risultato si ottiene a un costo molto ridotto per gli utenti del sistema. Poiché i
bit costano poco, per semplicità suggeriamo di utilizzare un sale di 256 bit.
Questo meccanismo va utilizzato con attenzione. Ci è capitato di vedere un sistema in
cui tutto era stato implementato perfettamente, ma poi qualche programmatore aveva
voluto migliorare l’interfaccia utente, informando più rapidamente l’utente del fatto
che la password digitata fosse corretta o meno. A questo scopo il programmatore aveva
registrato un checksum sulla password, invalidando l’intera procedura di sale e allungamento. Se il tempo di risposta è troppo lungo, si può ridurre il valore di r, ma occorre
assicurarsi che non sia possibile riconoscere se una password è corretta o meno senza
eseguire almeno r calcoli di hash.

21.3 Supporti di archiviazione portatili
I dati della chiave possono anche essere archiviati al di fuori del computer. Il supporto
più semplice è un foglio di carta su cui scrivere le password. Praticamente tutti l’hanno
utilizzato in una forma o in un’altra, anche per sistemi non crittografici come i siti web.
Molti utenti hanno almeno una mezza dozzina di password da memorizzare: semplicemente troppe, soprattutto per sistemi in cui le password si utilizzano di rado; per ricordarle,
quindi, le scrivono. Il limite di questa soluzione è che le password devono comunque essere
elaborate dagli occhi, dalla mente e dalle dita dell’utente a ogni utilizzo. Per non irritare
eccessivamente gli utenti, spingendoli a commettere troppi errori, questa tecnica può
essere utilizzata soltanto con password e passphrase di entropia relativamente ridotta.
Un progettista non deve mai prevedere questo metodo di archiviazione. Saranno gli
utenti a utilizzarlo, indipendentemente dalle regole fissate e dal modo in cui si crea il
sistema di password.

278   Capitolo 21

Una forma più avanzata di archiviazione è costituita dai supporti portatili di varie forme:
schede di memoria flash, carte magnetiche, chiavette USB o qualsiasi altro tipo di supporto digitale. I sistemi di archiviazione digitale offrono sempre lo spazio per contenere
almeno una chiave segreta da 256 bit, perciò si possono eliminare le password a bassa
entropia. Il supporto portatile diventa molto simile a una chiave: chiunque lo possieda
ottiene l’accesso, perciò è necessario custodirlo in modo sicuro.

21.4 Dispositivi sicuri
Una soluzione migliore – e più costosa – consiste nell’utilizzare un cosiddetto token sicuro.
Si tratta di un piccolo computer che Alice può portare sempre con sé. La forma esterna
dei token può variare notevolmente, da una smart card (del tutto simile a una carta di
credito) a un iButton, una chiavetta USB o una PC Card. Le proprietà principali sono
la presenza di memoria non volatile (cioè che mantiene i dati anche quando manca
l’alimentazione elettrica) e di una CPU.
Il token sicuro funziona principalmente come dispositivo di archiviazione portatile, ma
con alcuni miglioramenti relativi alla sicurezza. Innanzitutto l’accesso ai dati chiave può
essere limitato da una password o qualcosa di analogo. Prima che il token sicuro consenta
di utilizzare la chiave, è necessario fornirgli la password corretta. Il token può proteggersi
da solo nei confronti di attacchi di forza bruta disabilitando l’accesso dopo 3–5 tentativi
falliti. Alcuni utenti sbagliano troppo spesso a digitare la password e in questi casi sarà
necessario “resuscitare” il token, ma per questa operazione si possono utilizzare chiavi o
passphrase più lunghe e con maggiore entropia.
Tutto ciò fornisce una difesa a più livelli. Alice protegge il token fisico, per esempio
tenendolo nel portafoglio o nel portachiavi. Un attaccante deve innanzitutto rubare il
token, o almeno potervi accedere in qualche modo, e poi deve aprirlo fisicamente per
estrarre i dati che contiene, oppure trovare la password per sbloccarlo. I token spesso
sono progettati con un certo grado di resistenza alle manomissioni, per rendere ancora
più difficile un attacco di tipo fisico (questo non significa che sia impossibile manometterli, ma solo che l’operazione sarà più costosa. Alcuni token sono in grado di rilevare i
tentativi di manomissione e autodistruggersi).
I token sicuri attualmente offrono uno dei metodi migliori e più pratici per conservare
chiavi segrete. Sono relativamente poco costosi e sufficientemente piccoli da poter essere
comodamente portati con sé.
Un problema dell’uso pratico è dato dal comportamento degli utenti, che spesso lasceranno il token sicuro collegato al computer quando vanno a pranzo o in riunione.
Poiché gli utenti non amano che venga continuamente richiesto di inserire la password,
impostano il sistema in modo da consentire l’accesso per diverse ore dopo l’ultimo utilizzo
della password, perciò un attaccante deve semplicemente sedersi davanti al computer e
iniziare a utilizzare le chiavi segrete memorizzate nel token.
Potete provare a risolvere il problema attraverso la formazione degli utenti. Ecco così le
presentazioni video “Sicurezza aziendale in ufficio”, il bruttissimo poster “Porta il token
a pranzo” e i messaggi del tipo: “Se trovo ancora il tuo token collegato senza nessuno
presente, ti toccherà un’altra ramanzina come questa”. Ma si possono utilizzare anche
altri mezzi. Assicuratevi che il token non faccia solo da chiave di accesso ai dati digitali,
ma anche da serratura delle porte dell’ufficio, in modo che gli utenti debbano portarlo

Come conservare i segreti    279

con sé per poter rientrare. Impostate la macchina del caffè in modo che richieda l’utilizzo
del token. Queste strategie spingono i dipendenti a portare il token alla macchinetta
invece di lasciarlo collegato al computer mentre sono assenti. A volte la sicurezza è data
da misure semplici come queste, che però funzionano molto meglio di altri mezzi per
imporre delle regole di comportamento.

21.5 Interfaccia utente sicura
Il token sicuro presenta un punto debole significativo. La password utilizzata da Alice deve
essere digitata al PC o in qualche altro dispositivo. Se il PC è fidato non c’è problema, ma
sappiamo tutti che i PC non sono poi così sicuri. In effetti, il motivo principale che porta
a non memorizzare le chiavi di Alice nel PC è proprio perché non ci si fida abbastanza
della macchina. Si può ottenere un livello di sicurezza molto maggiore se il token stesso
dispone di un’interfaccia utente integrata e sicura. Pensate a un token sicuro dotato di
tastiera e schermo integrati. Ora la password, o più probabilmente un PIN, può essere
inserito direttamente nel token, senza la necessità di ricorrere a un dispositivo esterno
che deve essere fidato.
La presenza di una tastiera sul token protegge il PIN da eventuali compromissioni. Naturalmente, una volta che il PIN è stato digitato, la chiave arriva nel PC che può farne
di tutto, perciò la sicurezza complessiva del PC rimane un fattore limitante.
Per evitare ciò è necessario che i processi crittografici che coinvolgono la chiave siano
inseriti direttamente nel token. Ciò richiede l’inserimento di codice specifico dell’applicazione. Il token diventa rapidamente un computer vero e proprio, ma un computer
fidato che l’utente può portare ovunque. Questo computer può implementare sul token stesso la parte di ogni applicazione che risulta critica per la sicurezza. Lo schermo
a questo punto diventa fondamentale, perché è utilizzato per mostrare all’utente quale
azione sta autorizzando digitando il proprio PIN. Nel progetto tipico, l’utente utilizza
la tastiera e il mouse del PC per gestire l’applicazione. Quando deve essere autorizzato
un pagamento bancario, per esempio, il PC invia i dati al token, che visualizza l’importo
e altri dettagli della transazione, quindi l’utente autorizza la transazione in questione
digitando il proprio PIN. Il token allora firma i dettagli della transazione e il PC completa la parte restante.
A oggi i token dotati di interfaccia utente sicura sono troppo costosi per la maggior
parte delle applicazioni. Forse il dispositivo che vi si avvicina di più è uno smartphone o
PDA.Tuttavia gli utenti spesso trasferiscono programmi sugli smartphone che non sono
originariamente progettati come dispositivi sicuri e perciò non offrono una sicurezza
molto maggiore di un PC. Ci auguriamo che i token con interfaccia utente sicura si
diffonderanno maggiormente in futuro.

21.6 Biometria
A questo punto, per divertirci davvero, possiamo introdurre la biometria. Si potrebbe
integrare uno scanner di impronte digitali o dell’iride nel token sicuro. A oggi i dispositivi biometrici non sono di grande utilità. Gli scanner delle impronte digitali hanno
costi ragionevoli, ma il livello di sicurezza che forniscono non è particolarmente elevato,

280   Capitolo 21

in generale. Nel 2002 il crittografo Tsutomu Matsumoto, insieme a tre suoi studenti,
mostrò come fosse stato in grado di ingannare tutti gli scanner di impronte digitali che
era riuscito a procurarsi, utilizzando soltanto materiali che si trovano in casa e hobbistici
[87]. Anche per creare un’impronta falsa a partire da una latente (per esempio quella che
si lascia su una qualsiasi superficie lucida) è appena più difficile di un progetto hobbistico
per un bravo studente della scuola superiore.
La vera sorpresa, per noi, non fu tanto il fatto che gli scanner potevano essere ingannati,
ma che ingannarli fosse così incredibilmente semplice e poco costoso. L’industria della
biometria ha sempre magnificato il livello di sicurezza dell’identificazione biometrica, non
ha mai parlato di quanto fosse facile falsificare le impronte digitali. Poi improvvisamente
salta fuori un matematico (e nemmeno un esperto di biometria) e fa crollare tutto. Un
recente articolo del 2009 mostra che questi aspetti rimangono problematici [3].
In ogni caso, anche se si possono ingannare facilmente, gli scanner di impronte digitali
possono essere molto utili. Supponete di avere un token sicuro con un piccolo schermo,
una piccola tastiera e uno scanner di impronte digitali. Per arrivare alla chiave, un attaccante deve ottenere il controllo fisico del token, ottenere il PIN e falsificare l’impronta
digitale. È parecchio lavoro, più di quanto serva con le soluzioni descritte in precedenza.
Probabilmente questo è il migliore schema di archiviazione della chiave che siamo in
grado di attuare, oggi. D’altra parte, questo token sicuro sta diventando piuttosto costoso,
perciò non potrà essere utilizzato da molte persone.
Gli scanner di impronte digitali potrebbero anche essere utilizzati per sistemi a basso livello
di sicurezza. Toccare uno scanner con un dito è un’operazione molto rapida, perciò si
può tranquillamente chiedere agli utenti di eseguirla relativamente spesso. Uno scanner
di impronte digitali, quindi, potrebbe essere utilizzato per aumentare la fiducia che nel
fatto che le azioni che il computer sta per eseguire siano autorizzate dalla persona giusta.
Per i dipendenti di un’azienda sarà più difficile prestare le proprie password a un collega.
Anziché cercare di bloccare gli attaccanti più sofisticati, lo scanner di impronte digitali
potrebbe essere utilizzato per far cessare violazioni occasionali delle regole di sicurezza.
Potrebbe essere un contributo alla sicurezza ancora più importante di quello ottenuto
cercando di utilizzare lo scanner come strumento a elevato livello di sicurezza.

21.7 Sistemi SSO (Single Sign-On)
Poiché l’utente medio ha tante password, sarebbe utile creare un sistema SSO (Single
Sign-On), o a identificazione singola. L’idea è quella di fornire ad Alice una sola password
master che a sua volta è utilizzata per cifrare tutte le varie password delle applicazioni.
Perché il sistema funzioni, tutte le applicazioni devono comunicare con il sistema SSO.
Ogni volta che un’applicazione richiede una password, non dovrebbe chiederla all’utente,
ma al programma SSO. L’applicazione su larga scala di questo meccanismo presenta diversi
problemi. Pensate soltanto a tutte le applicazioni che dovrebbero essere modificate per
fare in modo che ottengano automaticamente le loro password dal sistema SSO.
Un’idea più semplice è quella di utilizzare un programmino che registri le password in
un file di testo. Alice digita la password master e poi utilizza la funzione di copia e incolla
per copiare le password dal programma SSO alle applicazioni. Bruce Schneier ha progettato un programma gratuito denominato Password Safe che fa esattamente questo. In
sostanza è semplicemente una versione digitale cifrata del foglio di carta dove si scrivono

Come conservare i segreti    281

le password. È utile, e migliore rispetto al foglietto di carta se si utilizza sempre lo stesso
computer, ma non è la soluzione definitiva che l’idea di sistema SSO vorrebbe fornire.

21.8 Rischio di perdita dei dati
Che cosa succede se il token sicuro si rompe? O se il foglio di carta con le password
viene lasciato in una tasca e messo in lavatrice? Perdere le chiavi segrete è sempre un
evento negativo. Il costo può variare, dalla necessità di ripetere la registrazione per ogni
applicazione in modo da ottenere una nuova chiave, al perdere per sempre l’accesso a
dati importanti. Se cifrate la tesi di dottorato su cui avete lavorato per cinque anni con
una chiave segreta, e poi perdete la chiave, non avete più la vostra tesi, ma soltanto un
file pieno di bit dall’aspetto confuso. Ahia!
È difficile realizzare un sistema di archiviazione delle chiavi che sia facile da utilizzare
e nello stesso tempo altamente affidabile. Una buona regola pratica è quella di separare
queste funzioni. Mantenete due copie della chiave – una che sia facile da utilizzare e
un’altra molto affidabile. Se il sistema facile da usare dovesse perdere la chiave, potrete
recuperarla dal sistema più affidabile. Quest’ultimo potrebbe essere anche molto semplice,
per esempio un foglio di carta conservato in una cassetta di sicurezza di una banca.
Naturalmente il sistema di archiviazione affidabile va utilizzato con attenzione. Per definizione, sarà utilizzato per conservare tutte le vostre chiavi, perciò costituirà un obiettivo
molto allettante per un attaccante. Dovrete eseguire un’analisi dei rischi per determinare
se sia meglio avere numerosi luoghi di conservazione delle chiavi affidabili, o un unico
luogo molto grande.

21.9 Secret sharing
Esistono chiavi che è necessario conservare in modo super sicuro, per esempio la chiave
di root privata della vostra CA. Come abbiamo visto, conservare un segreto in modo
sicuro può essere difficile. Conservarlo in modo sicuro e affidabile lo è ancora di più.
Esiste una sola soluzione crittografica che può aiutare nella conservazione di chiavi
segrete. Si chiama secret sharing [117], traducibile letteralmente in “condivisione del
segreto”, un nome poco azzeccato perché sembrerebbe indicare che il segreto viene
condiviso con più persone, mentre non è così. L’idea è quella di suddividere il segreto
in diverse quote (share). È possibile farlo in modo che, per esempio, siano necessarie tre
quote su cinque per poter recuperare il segreto. Poi si fornisce una quota a ciascuno dei
dipendenti anziani del reparto IT; tre di essi possono recuperare il segreto. Il trucco è
quello di fare i modo che due persone qualsiasi, messe insieme, non sappiano assolutamente nulla della chiave.
I sistemi secret sharing sono molto interessanti dal punto di vista accademico. Ognuna
delle quote viene memorizzata utilizzando una delle tecniche discusse in precedenza. Una
regola k-su-n combina alta sicurezza (sono necessarie almeno k persone per recuperare la
chiave) e alta affidabilità (si possono perdere n – k senza effetti negativi). Esistono anche
schemi di secret sharing più stravaganti che consentono di impostare regole di accesso
più complesse, sulla falsariga di (Alice e Bob) o (Alice e Carol e David).

282   Capitolo 21

Nella vita reale gli schemi di secret sharing sono utilizzati raramente, perché sono troppo
complessi. Sono complessi da implementare, ma soprattutto da amministrare e gestire.
Nella maggior parte delle aziende non c’è un gruppo di persone altamente responsabili
che non si fidino l’una dell’altra. Provate a dire ai membri del consiglio di amministrazione che ognuno di essi riceverà ciascuno un token sicuro con una quota della chiave,
e che dovranno presentarsi anche alle 3 del mattino di domenica in caso di emergenza.
Ah sì, e poi non dovranno fidarsi l’uno dell’altro, ma conservare la loro quota in modo
sicuro, proteggendola anche dagli altri membri del consiglio. E dovranno recarsi nel
locale di gestione delle chiavi per ricevere una nuova quota ogni volta che un membro
si aggiunga al consiglio o lo lasci. Nella pratica questo significa che l’idea di rivolgersi ai
membri del consiglio di amministrazione non può nemmeno essere presa in considerazione. Anche l’amministratore delegato non è una scelta utile, perché in genere tende a
viaggiare molto. Presto non rimarranno che i due o tre impiegati con maggiore anzianità
del reparto IT. Loro potrebbero utilizzare uno schema di secret sharing, ma la spesa e la
complessità rende questa soluzione poco attraente.
Perché non utilizzare qualcosa di molto più semplice, come una cassaforte? Le soluzioni
fisiche come casseforti e cassette di sicurezza hanno diversi vantaggi. Tutti capiscono
come funzionano, perciò non serve un lungo addestramento. Sono già state ampiamente testate, mentre la procedura di ricostruzione del segreto nello schema secret sharing
è difficile da testare perché richiede un gran numero di interazioni utente, e nessuno
vorrebbe avere un bug nel processo di ricostruzione del segreto che portasse a perdere
la chiave di root della CA.

21.10 Cancellazione dei segreti
Ogni segreto di lungo termine alla fine dovrà essere cancellato. Non appena un segreto non serve più, si dovrebbe cancellare il supporto in cui è memorizzato, per evitare
eventuali future violazioni. Abbiamo discusso la cancellazione della memoria nel Paragrafo 8.3. La cancellazione di segreti a lungo termine da supporti di memorizzazione
permanente è molto più difficile.
Gli schemi per conservare segreti di lungo termine che abbiamo trattato in questo capitolo
utilizzano una varietà di tecnologie di memorizzazione dei dati: dalla carta ai dischi fissi
alle chiavette USB. Nessuna di queste tecnologie dispone di una funzionalità di cancellazione documentata che garantisca che i dati cancellati non siano più recuperabili.

21.10.1 Carta
Per distruggere una password scritta su un foglio di carta, generalmente si distrugge il
foglio. Un metodo è quello di bruciare il foglio di carta e poi macinare le ceneri per
ottenere una polvere fine, oppure mescolarle con un po’ d’acqua per farne una poltiglia.
Esistono anche gli appositi distruggi-documenti, ma molti di questi apparecchi lasciano
pezzi di carta sufficientemente grandi da rendere relativamente facile la ricostruzione
di una pagina.

Come conservare i segreti    283

21.10.2 Supporti magnetici
I supporti magnetici sono molto difficili da cancellare. Non c’è molta letteratura sull’argomento, i miglior articolo che conosciamo è di Peter Gutmann [57], anche se oggi
probabilmente risulterà obsoleto nei dettagli tecnici.
I supporti magnetici memorizzano i dati in piccole aree magnetizzate: la direzione della
magnetizzazione di un’area determina i dati codificati in essa. Quando i dati vengono sovrascritti, le direzioni delle magnetizzazioni cambiano per riflettere i nuovi dati.
Tuttavia, esistono diversi meccanismi che evitano la perdita definitiva dei vecchi dati.
La testina di lettura/scrittura che cerca di sovrascrivere i vecchi dati non è mai perfettamente allineata e tenderà a lasciare intatte alcune parti dei vecchi dati. La sovrascrittura
quindi non distrugge completamente i vecchi dati. Potete pensare a quando si ridipinge
un muro con una sola mano di pittura: il vecchio strato di vernice rimane vagamente
visibile sotto il nuovo. Inoltre, le aree magnetizzate possono allontanarsi dalla testina di
lettura/scrittura, spostandosi ai lati rispetto alla traccia o più in profondità nel materiale
magnetico, dove possono rimanere per parecchio tempo. I dati sovrascritti non sono
normalmente recuperabili con la normale testina di lettura/scrittura, ma un attaccante
che entri in possesso di una unità a disco e utilizzi strumenti specializzati potrebbe essere
in grado di recuperare i vecchi dati in tutto o in parte.
Nella pratica, il metodo migliore per cancellare un segreto è probabilmente quello di
sovrascriverlo ripetutamente con dati casuali. Esistono alcuni aspetti da tenere in considerazione, descritti di seguito.
• Ogni sovrascrittura dovrebbe utilizzare nuovi dati casuali. Alcuni ricercatori hanno
sviluppato particolari pattern di dati che dovrebbero risultare migliori per cancellare
vecchi dati, ma la scelta dei pattern dipende dai dettagli precisi del disco interessato.
L’utilizzo di dati casuali potrebbe richiedere più passaggi di sovrascrittura per ottenere
lo stesso effetto, ma funziona in tutti i casi e quindi è più sicuro.
• Va sovrascritta l’effettiva locazione in cui era conservato il segreto. Se ci si limita a
modificare un file scrivendo nuovi dati su di esso, il file system potrebbe decidere di
registrare i nuovi dati in una locazione diversa, lasciando così intatti i dati originali.
• Assicuratevi che ogni passaggio di sovrascrittura sia effettivamente scritto su disco e
non solo in una delle cache. Le unità a disco dotate di cache di scrittura sono particolarmente pericolose, perché potrebbero memorizzare nella cache i nuovi dati e
raccogliere più operazioni di sovrascrittura in una sola a scopo di ottimizzazione.
• È probabilmente una buona idea cancellare un’area che inizi ben prima dei dati segreti
termini ben dopo di essi. Poiché la velocità di rotazione del disco non è mai perfettamente costante, i nuovi dati non saranno perfettamente allineati ai precedenti.
Per quanto di nostra conoscenza, non esistono informazioni attendibili su quanti passaggi di sovrascrittura siano richiesti, ma non vi è motivo per scegliere un numero
piccolo. Dovete soltanto cancellare una singola chiave (se avete una grande quantità di
dati segreti, memorizzateli cifrati con una chiave, e poi cancellate soltanto la chiave).
Riteniamo che 50 o 100 passaggi di sovrascrittura con dati casuali siano un numero del
tutto ragionevole.
È teoricamente possibile cancellare un nastro o un disco utilizzando un apparecchio di
demagnetizzazione (degaussing).Tuttavia, i moderni supporti magnetici ad alta densità hanno una tale resistenza al degaussing che questo metodo non è più affidabile. Nella pratica
gli utenti non hanno accesso a questi apparecchi, perciò non è il caso di considerarli.

284   Capitolo 21

Anche se si eseguono numerose sovrascritture, un attaccante altamente specializzato e
con parecchi fondi a disposizione potrebbe riuscire a recuperare il segreto da un supporto magnetico. Per distruggere completamente i dati, probabilmente sarà necessario
distruggere il supporto stesso. Se lo strato magnetico è protetto da un involucro di plastica
(dischetti, nastri), potete rimuoverlo e poi bruciare il supporto. Nel caso di un disco
fisso potete utilizzare una sabbiatrice per rimuovere lo strato magnetico dai piatti, o un
saldatore a fiamma per liquefarli. È improbabile che si riesca a convincere gli utenti ad
adottare soluzioni così estreme, perciò il metodo delle sovrascritture ripetute è quello
più pratico.

21.10.3 Supporti a stato solido
La cancellazione di memorie non volatili, come EPROM, EEPROM e flash, pone
problerni analoghi. La sovrascrittura dei dati vecchi non elimina tutte le tracce, e vanno
anche considerati i meccanismi di mantenimento dei dati descritti nel Paragrafo 8.3.4.
Anche qui la sovrascrittura ripetuta del segreto con dati casuali è l’unica soluzione pratica,
ma non è affatto perfetta. Appena un supporto a stato solido non serve più, andrebbe
distrutto.

21.11 Esercizi
Esercizio 21.1 Studiate in che modo le password di login sono memorizzate sulla vostra
macchina. Scrivete un programma che, data una password registrata (cifrata o sottoposta
a hash) effettui una ricerca esaustiva della password reale. Quanto tempo servirebbe al
vostro programma per una ricerca esaustiva sul primo milione di password?
Esercizio 21.2 Studiate in che modo sono memorizzate le chiavi private con GNU
Privacy Guard (GPG). Scrivete un programma che, data una chiave privata GPG cifrata,
effettui una ricerca esaustiva della password e recuperi la chiave. Quanto tempo servirebbe
al vostro programma per una ricerca esaustiva sul primo milione di password?
Esercizio 21.3 Considerate un sale di 24 bit. Dato un gruppo di 64 utenti, vi aspettate
che due di essi abbiano lo stesso sale? E su 1024 utenti? 4096? 16.777.216 utenti?
Esercizio 21.4 Trovate un nuovo prodotto o sistema che mantenga segreti a lungo
termine. Potrebbe essere lo stesso prodotto o sistema che avete analizzato per l’Esercizio
1.8. Effettuate una revisione della sicurezza di tale prodotto o sistema secondo le indicazioni del Paragrafo 1.12, ma questa volta concentrandovi su aspetti legati al modo in
cui il sistema potrebbe memorizzare i segreti in questione.

Parte V

Miscellanea

In questa parte
• Capitolo 22 Standard e brevetti
• Capitolo 23 Rivolgersi agli esperti

Capitolo 22

Standard e brevetti

A margine della crittografia in sé ci sono due elementi
che è necessario conoscere: gli standard e i brevetti.
In questo capitolo presentiamo la nostra visione su
entrambi.

22.1 Standard
Gli standard sono un’arma a doppio taglio. Da un
lato, nessuno vi potrà accusare per l’utilizzo di uno
standard, come abbiamo già detto a proposito di AES.
Dall’altro, molti standard di sicurezza non funzionano.
È un vero paradosso. In questo libro ci occupiamo
degli aspetti di progettazione della crittografia, ma in
qualsiasi progetto di crittografia si incontrano degli
standard, quindi è necessario conoscerli.

22.1.1 I l processo di creazione
degli standard
Per coloro che non sono mai stati coinvolti nelle
procedure di sviluppo degli standard, descriviamo
innanzitutto il modo in cui molti standard vengono
creati. Si parte da un ente di standardizzazione, come
l’Internet Engineering Task Force (IETF), l’Institute
of Electrical and Electronics Engineers (IEEE), l’International Organization for Standardization (ISO)
o l’European Committee for Standardization (CEN).
Questi enti istituiscono un comitato in base alla
percezione dell’esigenza di un nuovo standard o di
una versione migliorativa di uno standard esistente.
Il comitato può avere varie denominazioni: gruppo

Sommario
22.1 Standard
22.2 Brevetti

288   Capitolo 22

di lavoro, gruppo di progetto o altro. Talvolta ci sono strutture gerarchiche di comitati,
ma il concetto di base rimane lo stesso. I componenti del comitato sono generalmente
volontari. Occorre fare richiesta per parteciparvi, e praticamente chiunque può essere
accettato. Spesso ci sono alcuni ostacoli procedurali da superare, ma non c’è una vera
selezione dei membri. Il numero dei componenti può arrivare a diverse centinaia, ma
i grandi comitati sono suddivisi in sottocomitati più piccoli (detti gruppi di progetto,
gruppi di studio o altro). La maggior parte del lavoro viene svolto in un comitato che
può arrivare a qualche dozzina di persone.
I comitati per la standardizzazione si riuniscono regolarmente, ogni due o tre mesi.Tutti i
membri raggiungono una località e si incontrano in albergo per qualche giorno. Nei mesi
tra un incontro e l’altro, i membri del comitato lavorano, creando proposte, presentazioni
e così via. Durante gli incontri, il comitato stabilisce quale linea seguire. Solitamente un
unico coordinatore ha il compito di raccogliere tutte le proposte in un unico documento.
Creare uno standard è un processo lento e spesso richiede diversi anni.
Chi riesce a far parte di questi comitati? I membri devono sostenere dei costi. Oltre al
tempo necessario, i viaggi e gli alberghi non sono economici. Perciò tutti coloro che
partecipano vengono inviati a spese delle aziende, che del resto hanno parecchi motivi
per voler essere rappresentate.A volte desiderano vendere prodotti che devono interagire
con i prodotti di altre aziende; ciò richiede degli standard, e il modo migliore per tenere
traccia del processo di standardizzazione è quello di parteciparvi. Le aziende desiderano
anche tenere d’occhio i loro concorrenti. Nessuno vuole che il proprio concorrente
compili lo standard, perché si adopererebbe per creare uno svantaggio competitivo per
gli altri, magari orientandolo verso la propria tecnologia o le proprie esigenze, o inserendo tecniche specifiche per cui possiede il brevetto. Talvolta le aziende non vogliono
uno standard, quindi si presentano agli incontri del comitato con lo scopo di rallentare
il processo in modo che le loro soluzioni proprietarie abbiano il tempo di imporsi sul
mercato. Nella vita reale queste motivazioni, e molte altre ancora, sono tutte mescolate
insieme in proporzioni variabili a creare un ambiente politico molto complesso.
Non sorprende che un discreto numero di comitati di standardizzazione fallisca lo scopo.
Non producono niente, o producono qualcosa di assolutamente pessimo, o finiscono per
trovarsi in situazioni di stallo ed essere superati dal mercato, e a quel punto definiscono
come standard qualsiasi sistema sia già diffuso sul mercato. I comitati di successo riescono
a produrre un documento di standard dopo pochi anni.
Una volta istituito lo standard, tutti lo implementano. Ciò conduce, naturalmente, a
sistemi che non interagiscono, quindi occorre un processo secondario in cui venga
verificata l’interoperatività e le diverse aziende adattano la loro implementazione per
lavorare insieme.
Questo processo presenta diversi problemi. La struttura politica del comitato pone
pochissima attenzione alla creazione di uno standard di buon livello. La questione più
importante è raggiungere un accordo. Lo standard è completato quando tutti sono
ugualmente insoddisfatti del risultato. Per mettere a tacere le diverse fazioni, gli standard devono prevedere molte opzioni, funzionalità estese, alternative inutili e così via.
E dato che ciascuna fazione ha le proprie idee, opinioni e punti d’interesse, il miglior
compromesso è spesso contraddittorio. Molti standard sono internamente incoerenti, o
addirittura in contrasto con se stessi.
Tutto questo processo viene reso ancora più complicato dal fatto che le aziende creano
implementazioni mentre il processo di standardizzazione è ancora in corso, sulla base di

Standard e brevetti   289

versioni in bozza dello standard. Ciò rende ancora più difficile apportare delle modifiche,
perché qualcuno ha già implementato lo standard e non desidera farlo nuovamente. Naturalmente, aziende diverse implementano le cose in maniera diversa, e poi si scontrano
nel comitato perché lo standard venga adattato in modo da corrispondere alle loro implementazioni.Talvolta l’unico compromesso è quello di scegliere l’elemento che nessuna
azienda ha implementato, soltanto per assicurarsi che tutti siano ugualmente scontenti.
Il merito tecnico non compare assolutamente in questo tipo di discussione.

22.1.1.1 Leggibilità dello standard
Uno dei risultati è che la maggior parte degli standard è estremamente difficile da leggere. Il documento di standard è un progetto del comitato, e ci sono poche pressioni
all’interno del processo perché risulti sia chiaro, conciso, accurato, o soltanto leggibile.
In effetti, un documento altamente illeggibile è più facile da utilizzare, perché soltanto
pochi tra i membri del comitato lo capiranno, e potranno lavorare su di esso senza essere
disturbati dagli altri. Scavare tra centinaia di pagine di documentazione mal scritta non è
divertente, e la maggior parte dei membri finisce per non leggere la versione completa,
limitandosi a verificare le parti limitate dello standard che sono di suo interesse.

22.1.1.2 Funzionalità
Come abbiamo già detto, la verifica dell’interoperatività deve sempre essere eseguita.
E naturalmente aziende diverse implementano elementi diversi. Molto spesso, ciò che
finisce per essere implementato è sottilmente diverso da ciò che è definito dallo standard,
e poiché ogni azienda è già sul mercato con i propri prodotti, talvolta è troppo tardi
per apportare delle modifiche. Abbiamo visto prodotti del marchio A che riconoscono
l’implementazione del marchio B attraverso deviazioni dallo standard, e quindi adattano
il loro comportamento per far funzionare il tutto.
Gli standard spesso comprendono un grandissimo numero di opzioni, ma l’implementazione effettiva ne utilizzerà soltanto un particolare gruppo, naturalmente con alcune
restrizioni ed estensioni, perché il documento di standard stesso descrive qualcosa che
non funziona. E le differenze tra le implementazioni effettive e lo standard, naturalmente,
non sono documentate.
In generale l’intero processo funziona – più o meno – ma soltanto per la funzionalità
centrale. Una rete wireless consentirà di connettersi, ma è improbabile che la funzionalità di gestione possa andare bene per dispositivi di produttori diversi. Semplici pagine
HTML vengono visualizzate correttamente da tutti i browser, ma caratteristiche di layout
più avanzate danno risultati diversi nei diversi browser. Tutti siamo talmente abituati a
queste situazioni che difficilmente ce ne stupiamo.
Si tratta di una situazione infausta. Come settore, sembriamo incapaci di creare standard
che siano almeno leggibili o corretti, per non citare l’interoperatività di prodotti diverse
per caratteristiche che non siano soltanto la funzionalità di base.

22.1.1.3 Sicurezza
I problemi descritti indicano che il metodo tipico di produzione degli standard semplicemente non funziona per scopi di sicurezza. Nel campo della sicurezza, occorre fronteggiare
un attacco attivo che scandaglierà anche l’angolo più remoto dello standard. La sicurezza
dipende dall’elemento più debole: qualsiasi singolo errore può essere fatale.

290   Capitolo 22

Abbiamo già sottolineato l’importanza della semplicità. Il processo instaurato dal comitato
preclude la semplicità e produce invariabilmente qualcosa di più complesso rispetto a ciò
che qualunque membro del comitato stesso possa comprendere appieno. Già per questa
ragione, il risultato non può mai essere sicuro.
Quando abbiamo discusso di questo problema con alcune persone che si occupano di
standardizzazione, spesso abbiamo ricevuto risposte del tipo: “Be’, i tecnici vogliono
sempre uno standard perfetto”...“Le realtà politiche sono tali che dobbiamo arrivare a un
compromesso”...“Il sistema funziona così”... “Considera quello che abbiamo ottenuto”...
“Le cose funzionano piuttosto bene”. Nel campo della sicurezza, tutto ciò non basta. Il
fatto stesso che sia necessaria la verifica di interoperatività dopo che lo standard è stato
istituito dimostra che gli standard dei comitati non funzionano dal punto di vista della
sicurezza. Se la parte funzionale dello standard (quella facile) non è di livello abbastanza
elevato da consentire la produzione di sistemi interoperabili senza necessità di verifica,
allora la parte riguardante la sicurezza non potrà ottenere la sicurezza senza verifiche. E,
come sappiamo, non è possibile verificare la sicurezza. Certo, si potrebbe creare un’implementazione che comprenda un sottogruppo delle funzionalità dello standard che sia
anch’esso sicuro, ma ciò non è sufficiente per uno standard di sicurezza. Uno standard di
sicurezza implica che, se lo si segue, si ottiene un certo livello di sicurezza. La materia è
semplicemente troppo complessa per essere lasciata a un comitato. Quindi, ogniqualvolta
qualcuno suggerisce di utilizzare uno standard crittografico compilato da un comitato,
noi siamo sempre estremamente riluttanti a farlo.
Esistono pochi standard utili nel nostro campo, nessuno dei quali è stato prodotto da
un comitato. Talvolta si tratta soltanto di un piccolo gruppo di persone che creano un
unico progetto coerente. E a volte il risultato viene adottato come standard evitando
tanti compromessi politici. Questi standard talvolta sono eccellenti. Esaminiamo i due
più importanti nel seguito.

22.1.2 SSL
SSL è il protocollo di sicurezza utilizzato dai browser web per connettersi in modo sicuro
ai server web. La prima versione utilizzata in maniera diffusa fu SSL 2, che presentava
diverse falle. La versione migliorata SSL 3 [53] venne progettata da tre persone senza
nessuna procedura tramite comitato. SSL 3 è ampiamente utilizzato e viene generalmente
riconosciuto come un buon protocollo.
Attenzione: SSL è un buon protocollo, ma ciò non significa che qualsiasi sistema che
utilizzi SSL sia sicuro. SSL si basa su una PKI per autenticare il server, e il client PKI
contenuto nella maggior parte dei browser è talmente tollerante che il livello di sicurezza
generale è piuttosto basso. Uno dei nostri browser ha circa 150 diversi certificati root
da 70 diverse CA. Quindi, prima ancora di iniziare a esaminare eventuali attacchi attivi,
ci sono 35 diverse organizzazioni sparse per il mondo a cui dobbiamo affidare tutte le
nostre informazioni nel web.
SSL non è stato mai veramente standardizzato. È stato semplicemente implementato da
Netscape, ed è diventato uno standard de facto. La standardizzazione e il successivo sviluppo di SSL sono state effettuate sotto la denominazione TLS da un gruppo di lavoro
dell’IETF. Le modifiche apportate sembrano di scarsa rilevanza, e non ci sono ragioni
per credere che TLS non sia un protocollo altrettanto buono rispetto a SSL 3. Tuttavia,

Standard e brevetti   291

considerati i risultati recenti ottenuti dall’IETF con protocolli di sicurezza come IPsec
[51], si intravede il rischio dell’effetto del comitato che impone se stesso e complica
inutilmente uno standard di buon livello.

22.1.3 AES: standardizzazione attraverso la concorrenza
Per noi, AES è l’esempio sfolgorante di come standardizzare i sistemi di sicurezza. AES
non è un progetto di un comitato, ma piuttosto un progetto ottenuto attraverso la
concorrenza. Il nuovo processo di standardizzazione SHA-3 si sta dimostrando molto
simile. Il processo è piuttosto semplice: innanzitutto occorre specificare ciò che il sistema
dovrebbe raggiungere; lo sviluppo delle specifiche può essere eseguito in un gruppo
ragionevolmente piccolo con indicazioni provenienti da molte fonti diverse .
Il passo successivo è una richiesta di proposte. Si chiede agli esperti di sviluppare soluzioni che soddisfino le condizioni richieste. Una volta ricevute le proposte, tutto ciò
che rimane è selezionarle. Si tratta di una competizione diretta in cui si giudica in base
a una serie di criteri. Se si stabilisce che la sicurezza è il criterio principale, i partecipanti
hanno un interesse assoluto a scoprire i punti deboli nelle proposte degli altri concorrenti. Con un po’ di fortuna, ciò porta a riscontri molto utili. In altre situazioni potrebbe
essere necessario pagare cifre elevate per ottenere valutazioni della sicurezza effettuate
da esperti esterni.
Alla fine, se le cose vanno bene, si è in grado di selezionare un’unica proposta, senza
modifiche o con piccoli aggiustamenti. Non è il momento di fondere proposte diverse;
ciò condurrebbe soltanto a un altro progetto in stile comitato. Se nessuna delle proposte
soddisfa i requisiti, e sembra possibile creare qualcosa di meglio, probabilmente è il caso
di chiedere nuove proposte.
Questo è esattamente il modo in cui NIST ha gestito il concorso per AES, e ha funzionato benissimo. Le 15 proposte originali sono state valutate nella prima fase e ridotte a
cinque finalisti. Una seconda fase di valutazione dei finalisti ha portato alla selezione del
vincitore. Chiunque dei cinque finalisti avrebbe comunque rappresentato uno standard
di buon livello, e sicuramente uno standard migliore di qualsiasi progetto di comitato. Il
nostro appunto principale riguarda il fatto che il processo di standardizzazione di AES
potrebbe essere stato un po’ troppo veloce, e forse il tempo potrebbe essere stato insufficiente per analizzare nel dettaglio tutte le proposte dei finalisti.Tuttavia, il processo era
veramente ottimo.
Il modello della concorrenza per la standardizzazione funziona soltanto se è disponibile
un numero sufficiente di esperti per creare almeno alcuni progetti concorrenti.Tuttavia,
se non si dispone di un numero sufficiente di esperti per produrre diverse proposte,
non si dovrebbe standardizzare alcun sistema di sicurezza. Per ragioni di semplicità e
coerenza, che sono elementi cruciali per la sicurezza generale, un sistema di sicurezza
deve essere progettato da un piccolo gruppo di esperti. Successivamente sono necessari
altri esperti per analizzare la proposta e metterla alla prova, cercando i punti deboli. Per
avere qualche speranza di ottenere un buon risultato, a prescindere dal processo utilizzato,
servono esperti in numero sufficiente da formare almeno tre gruppi di proposte. Con
questa quantità di esperti, si dovrebbe utilizzare il modello della concorrenza, poiché ha
dimostrato di poter produrre buoni standard di sicurezza.

292   Capitolo 22

22.2 Brevetti
Abbiamo discusso a lungo del ruolo dei brevetti nella crittografia nella prima edizione di
questo libro. Da allora l’ecosistema che ruota intorno ai brevetti è cambiato, e abbiamo
imparato molto. In questo libro abbiamo scelto di non offrire indicazioni specifiche sui
brevetti. Tuttavia desideriamo che siate consapevoli del fatto che i brevetti hanno un
ruolo nella crittografia. I brevetti possono avere un impatto sulla scelta dei protocolli
crittografici che intendete o meno utilizzare.Vi consigliamo di contattare il vostro legale
per indicazioni specifiche sui brevetti.

Capitolo 23

Rivolgersi agli esperti

C’è qualcosa di strano nel campo della crittografia:
tutti pensano di saperne a sufficienza da poter progettare e creare un proprio sistema. Non si chiede
mai a uno studente di fisica del secondo anno di
progettare un impianto per l’energia nucleare. Non
ci lasceremmo operare da un tirocinante che sostenga di aver trovato un metodo rivoluzionario per la
chirurgia cardiaca.Tuttavia, persone che hanno letto
un libro o due pensano di poter progettare il proprio
sistema crittografico. Ancora peggio, a volte sono in
grado di convincere dirigenti, imprenditori e anche
qualche cliente che il loro progetto sia il più accurato
mai apparso sulla faccia della terra.
Tra i professionisti della crittografia, il primo libro
di Bruce, Applied Cryptography [111, 112] è famoso e
anche famigerato. È famoso per aver portato la crittografia all’attenzione di decine di migliaia di persone.
È famigerato per i sistemi che queste persone hanno
poi progettato e implementato da soli.
Un esempio recente è 802.11, lo standard della rete
wireless. Il progetto iniziale comprendeva un canale
sicuro denominato WEP (Wired Equivalent Privacy)
per cifrare e autenticare le comunicazioni wireless.
Lo standard venne concepito da un comitato senza
alcun esperto di crittografia tra i membri. I risultati
furono spaventosi dal punto di vista della sicurezza.
La decisione di utilizzare l’algoritmo di cifratura
RC4 non fu la migliore, ma nemmeno un errore
fatale in sé. Tuttavia, RC4 è un cifrario a flusso e
necessita di un unico nonce. WEP non alloca bit
sufficienti per il nonce, con il risultato che lo stesso
valore di nonce deve essere riutilizzato, il che a sua
volta produce molti pacchetti cifrati con lo stesso

294   Capitolo 23

flusso chiave. Ciò va a detrimento delle proprietà di cifratura del cifrario a flusso RC4 e
consente a un attaccante astuto di violare la cifratura. Un errore più sottile fu quello di
non sottoporre ad hashing la chiave segreta e il valore nonce prima di utilizzarli come
chiave RC4, che alla fine portò ad attacchi di recupero delle chiavi [52]. Un checksum
CRC era utilizzato per l’autenticazione, ma, poiché i calcoli CRC sono lineari, era facile
(utilizzando nozioni di algebra lineare) modificare qualsiasi pacchetto senza alcuna possibilità di essere scoperti. Un’unica chiave condivisa veniva utilizzata per tutti gli utenti
di una rete, rendendo molto più difficoltosi gli aggiornamenti delle chiavi. La password
di rete veniva impiegata direttamente come chiave di cifratura per tutte le comunicazioni, senza utilizzare nessun tipo di protocollo di negoziazione della chiave. E infine, la
cifratura era disattivata per default, quindi la maggior parte delle implementazioni non
si preoccupava nemmeno di attivarla. WEP non veniva semplicemente violato; veniva
compromesso pesantemente.
Progettare un sostituto per WEP non fu semplice, perché doveva essere adattato al software esistente. Ma non c’era scelta, la sicurezza dello standard originale era inesistente.
Il sostituto fu WPA.
La storia di WEP non è eccezionale. È più noto rispetto alla maggior parte dei cattivi
progetti crittografici, perché 802.11 è un prodotto di grandissimo successo, ma si sono
verificate molte situazioni analoghe in altri sistemi. Come un collega disse una volta a
Bruce: “Il mondo è pieno di sistemi di sicurezza pessimi progettati da persone che hanno
letto Applied Cryptography”.
E questo libro potrebbe avere lo stesso effetto.
Ciò lo rende un libro molto pericoloso. Alcune persone leggeranno il nostro testo e
poi si butteranno subito nella stesura di un algoritmo o di un protocollo crittografico.
Quando avranno terminato, otterranno un prodotto che a loro sembrerà buono, e magari funzionerà, ma sarà sicuro? Può darsi che lavorino bene per il 70%. Se sono molto
fortunati, potranno arrivare al 90%. Ma un prodotto quasi giusto in crittografia non
serve a nulla. Un sistema di sicurezza è forte quanto lo è il suo elemento più debole; per
essere sicuro, deve essere corretto in ogni sua parte. E ci sono cose che semplicemente
non si possono apprendere leggendo un libro.
Allora perché abbiamo scritto questo libro, se c’è il rischio che porti a realizzare pessimi
sistemi? L’abbiamo scritto perché le persone che desiderano imparare a progettare sistemi
crittografici devono pur imparare da qualche parte, e non conosciamo altri libri adatti a
questo scopo. Considerate questo testo come un’introduzione al campo, anche se non si
tratta di un manuale. L’abbiamo scritto anche per gli altri tecnici coinvolti in un progetto.
Ogni parte di un sistema di sicurezza ha importanza critica, e tutti coloro che lavorano
a un progetto devono comprendere a livello base le questioni riguardanti la sicurezza e
le relative tecniche. I programmatori, i tester, chi scrive la documentazione, i dirigenti
e anche gli addetti alle vendite. Ciascuno ha la necessità di comprendere le questioni di
sicurezza a un livello che gli consenta di operare in maniera appropriata. Speriamo che
questo testo fornisca una base adeguata al lato pratico della crittografia.
Speriamo anche di aver instillato nei lettori un senso di paranoia professionale. Se avete
questa sensazione, avete imparato molto. Potete applicare la paranoia professionale a
tutti gli aspetti del vostro lavoro. Sarete estremamente scettici quando progetterete il
vostro protocollo o esaminerete quello di altri, e ciò potrà soltanto aiutarvi a migliorare
la sicurezza.

Rivolgersi agli esperti   295

Se ci permettete di congedarci con un consiglio, è quello di rivolgervi a veri esperti di
crittografia non appena possibile. Se i vostri progetti riguardano la crittografia, trarrete
grandi vantaggi dalle capacità di analisi di un progettista esperto. Coinvolgetene uno nei
vostri progetti fin dall’inizio. Prima consultate un esperto, meno costoso e difficile sarà
il cammino sul lungo periodo. Molte volte siamo stati contattati per progetti già avviati,
soltanto per rilevare falle in parti che erano state progettate o implementate da tempo.
Il risultato finale è sempre dispendioso, in termini di fatica, tempistica del progetto, e
costi, o in termini di sicurezza per l’utente del prodotto finale
Lavorare bene nella crittografia è diabolicamente difficile. Anche sistemi progettati da
veri esperti falliscono regolarmente. Non importa quanto siate intelligenti, o quanta
esperienza abbiate in altri campi: progettare e implementare sistemi crittografici richiede conoscenze ed esperienze specifiche, e l’unico modo per fare esperienza è quello di
provare e riprovare ancora. E ciò comporta, naturalmente, anche compiere degli errori.
E allora perché ricorrere a un esperto, se anch’egli sbaglia? Per lo stesso motivo per cui
si ricorre a un chirurgo qualificato per farsi operare. Il punto non è che gli esperti non
commettono errori; è che ne commettono molti di meno, e meno gravi. E operano
in maniera prudente, in modo che errori piccoli non conducano a risultati catastrofici;
sanno abbastanza da sbagliare bene.
Implementare sistemi crittografici è una specializzazione, quasi come progettarli. I progettisti di crittografia si possono ingaggiare; gli implementatori sono molto più difficili da
trovare, in parte perché ne serve un numero maggiore. Un unico progettista può creare
lavoro per dieci o venti implementatori. La maggior parte delle persone non pensa che
l’implementazione di crittografia sia una specializzazione lavorativa. I programmatori
proveranno a passare dalla programmazione di database al lavoro sulle GUI e all’implementazione crittografica. È vero che anche la programmazione di database e le GUI
sono specializzazioni, ma un programmatore esperto può, con un po’ di studio, ottenere
risultati ragionevolmente buoni in entrambi i campi. Questo non vale per l’implementazione di sistemi crittografici, in cui tutto deve essere corretto e ci si trova di fronte a
un attaccante che cerca di far andare tutto male.
Il modo migliore per implementare sistemi crittografici, a nostro parere, è quello di avere
programmatori competenti e ben formati per il loro compito. Questo libro potrebbe
far parte della loro formazione, ma servono soprattutto esperienza e l’attitudine mentale
della paranoia professionale. E come avviene per qualsiasi altro campo della tecnologia
informatica, servono anni prima di diventare davvero bravi. Dato il lungo tempo necessario per raccogliere questa esperienza, le persone esperte vanno trattate con grande
attenzione. Questo è un altro problema, la cui soluzione lasciamo volentieri ad altri.
Forse ancora più importante di questo libro, o di qualsiasi altro, è la cultura della progettazione.“Sicurezza innanzitutto” non dovrebbe essere soltanto uno slogan, ma un concetto
intessuto nel profondo del progetto e del gruppo di progettazione. Tutti devono vivere,
respirare, parlare e pensare alla sicurezza continuamente. È un obiettivo incredibilmente
difficile da raggiungere, ma non impossibile. DigiCash aveva un gruppo come questo
negli anni ’90. Il settore aereo ha una cultura della sicurezza altrettanto pervasiva. Si tratta
di qualcosa che non può essere raggiunto nel breve periodo, ma a cui certamente si può
tendere. Questo libro è soltanto un testo di base sulle questioni di sicurezza più importanti,
che si rivolge alle persone con qualifiche più tecniche all’interno del gruppo.
Come scrisse Bruce in Secrets and Lies: “La sicurezza è un processo, non un prodotto”.
Oltre alla cultura della sicurezza, è necessario anche un processo della sicurezza. Il settore

296   Capitolo 23

aereo dispone di un processo di sicurezza di ampio respiro. La gran parte del settore
informatico non dispone di un processo di produzione del software, tanto meno di un
processo per lo sviluppo di software di alta qualità, meno ancora di un processo per il
software della sicurezza. Scrivere un buon software che tratti di sicurezza è ampiamente
al di là dello stato dell’arte del nostro settore. Ciò non significa che dobbiamo arrenderci,
tuttavia, e ultimamente si sono fatti dei progressi. La tecnologia informatica diventa sempre
più cruciale per la nostra infrastruttura, la nostra libertà e la nostra sicurezza, dobbiamo
assolutamente continuare a migliorare la sicurezza dei nostri sistemi. Dobbiamo fare del
nostro meglio.
Speriamo che questo libro possa contribuire in qualche misura al miglioramento dei
sistemi di sicurezza, insegnando a coloro che lavorano nel campo gli elementi di base
della crittografia pratica.

Bibliografia

[1]

Ross Anderson, Eli Biham e Lars Knudsen. Serpent: A Proposal for the Advanced
Encryption Standard. In National Institute of Standards and Technology [98].
Vedi http://www.cl.cam.ac.uk/~rja14/serpent.html. [pag. 56]

[2]

Ross J. Anderson. Security Engineering: A Guide to Building Dependable Distributed Systems.
John Wiley & Sons, Inc., 2008. [pag. 18]

[3]

Claude Barral e Assia Tria. Fake Fingers in Fingerprint Recognition: Glycerin Supersedes
Gelatin. In Véronique Cortier, Claude Kirchner, Mitsuhiro Okada e Hideki Sakurada, a
cura di, Formal to Practical Security, volume 5458 di Lecture Notes in Computer Science, pagg.
57 – 69. Springer-Verlag, 2009. [pag. 309]

[4]

Mihir Bellare. New Proofs for NMAC and HMAC: Security Without Collision-Resistance.
In Cynthia Dwork, a cura di, Advances in Cryptology – CRYPTO 2006, volume 4117 di
Lecture Notes in Computer Science, pagg. 602 – 619. Springer-Verlag, 2006. [pag. 93]

[5]

Mihir Bellare, Ran Canetti e Hugo Krawczyk. Keying Hash Functions for Message
Authentication. In Koblitz [76], pagg. 1 – 15. [pagg. 93, 94]

[6]

Mihir Bellare, Joe Kilian e Phillip Rogaway. The Security of Cipher Block Chaining. In
Desmedt [31], pagg. 341 – 358. [pagg. 46, 92]

[7]

Mihir Bellare e Chanathip Namprempre.Authenticated Encryption: Relations Among Notions
and Analysis of the Generic Composition Paradigm. In Tatsuaki Okamoto, a cura di, Advances
in Cryptology – ASIACRYPT 2000, volume 1976 di Lecture Notes in Computer Science, pagg.
531 – 545. Springer-Verlag, 2000. [pag. 102]

[8]

Mihir Bellare e Phillip Rogaway. The Exact Security of Digital Signatures: How to Sign
with RSA and Rabin. In Ueli M. Maurer, a cura di, Advances in Cryptology – EUROCRYPT
1996, volume 1070 di Lecture Notes in Computer Science. Springer-Verlag, 1996. [pag. 206]

[9]

Mihir Bellare e Phillip Rogaway. Optimal Asymmetric Encryption: How to Encrypt
with RSA. In Alfredo De Santis, a cura di, Advances in Cryptology – EUROCRYPT 1994,
volume 950 di Lecture Notes in Computer Science, pagg. 92 – 111. Springer-Verlag, 2004.
[pag. 206]

[10]

Mihir Bellare e Phillip Rogaway. Introduction to Modern Cryptography, 2005. Disponibile
presso http://cseweb.ucsd.edu/users/mihir/cse207/classnotes.html. [pag. 18]

[11]

Charles H. Bennett e Gilles Brassard.An update on quantum cryptography. In G.R. Blakley
e David Chaum, a cura di, Advances in Cryptology, Proceedings of CRYPTO 84, volume 196
di Lecture Notes in Computer Science, pagg. 475 – 480. Springer-Verlag, 1984. [pag. 139]

298   Bibliografia

[12]

Daniel J. Bernstein. Cache-Timing Attacks on AES, 2005. Disponibile presso http://cr.yp.
to/antiforgery/cachetiming-20050414.pdf. [pag. 251]

[13]

Eli Biham. New Types of Cryptanalytic Attacks Using Related Keys. In Helleseth [61],
pagg. 398 – 409. [pag. 45]

[14]

Alex Biryukov, Orr Dunkelman, Nathan Keller, Dmitry Khovratovich e Adi Shamir. Key
Recovery Attacks of Practical Complexity on AESVariants With Up To 10 Rounds. Cryptology
ePrint Archive, Report 2009/374, 2009.Vedi http://eprint.iacr.org/2009/374. [pag. 55]

[15]

Alex Biryukov e Dmitry Khovratovich. Related-key Cryptanalysis of the Full AES-192
and AES-256. Cryptology ePrint Archive, Report 2009/317, 2009. Vedi http://eprint.
iacr.org/2009/317. [pag. 55]

[16]

Alex Biryukov, Dmitry Khovratovich e Ivica Nikoli. Distinguisher and Related-Key
Attack on the Full AES-256. In Shai Halevi, a cura di, Advances in Cryptology – CRYPTO
2009, volume 5677 di Lecture Notes in Computer Science, pagg. 231 – 249. Springer-Verlag,
2009. [pag. 55]

[17]

Jurjen Bos. Booting problems with the JEC computer. Personal communications, 1983.
[pag. 125]

[18]

Jurjen Bos. Practical Privacy. PhD thesis, Eindhoven University of Technology, 1992.
Disponibile presso http://www.macfergus.com/niels/lib/bosphd.html. [pagg. 179, 239, 245]

[19]

Gilles Brassard e Claude Crépeau. Quantum Bit Commitment and Coin Tossing Protocols.
In Menezes e Vanstone [89], pagg. 49 – 61. [pag. 139]

[20]

Karl Brincat e Chris J. Mitchell. New CBC-MAC forgery attacks. InV.Varadharajan eY. Mu, a
cura di, Information Security and Privacy, ACISP 2001, volume 2119 di Lecture Notes in Computer
Science, pagg. 3 – 14. Springer-Verlag, 2001. [pagg. 91, 92]

[21]

David Brumley e Dan Boneh. Remote Timing Attacks are Practical. In USENIX Security
Symposium Proceedings, 2003. [pag. 251]

[22]

Carolynn Burwick, Don Coppersmith, Edward D’Avignon, Rosario Gennaro, Shai Halevi,
Charanjit Jutla, Stephen M. Matyas Jr., Luke O’Connor, Mohammad Peyravian, David Safford
e Nevenko Zunic. MARS – a candidate cipher for AES. In National Institute of Standards
and Technology [98].Vedi http://www.research.ibm.com/security/mars.pdf. [pagg. 58, 250]

[23]

Christian Cachin. Entropy Measures and Unconditional Security in Cryptography. PhD thesis,
ETH, Swiss Federal Institute of Technology, Zürich, 1997.Vedi ftp://ftp.inf.ethz.ch/pub/
publications/dissertations/th12187.ps.gz. [pag. 142]

[24]

Lewis Carroll. The Hunting of the Snark:An Agony, in Eight Fits. Macmillan and Co., London,
1876. [pag. 126]

[25]

Florent Chabaud e Antoine Joux. Differential Collisions in SHA-0. In Hugo Krawczyk, a
cura di, Advances in Cryptology – CRYPTO ’98, volume 1462 di Lecture Notes in Computer
Science, pagg. 56 – 71. Springer-Verlag, 1998. [pag. 82]

[26]

Jean-Sébastien Coron, Yevgeniy Dodis, Cécile Malinaud e Prashant Puniya. MerkelDamgård Revisited: How to Construct a Hash Function. In Shoup [119], pagg. 430 – 448.
[pagg. 86, 87]

[27]

Joan Daemen eVincent Rijmen.AES Proposal: Rijndael. In National Institute of Standards
and Technology [98]. [pag. 55]

[28]

I.B. Damgård, a cura di. Advances in Cryptology – EUROCRYPT ’90, volume 473 di Lecture
Notes in Computer Science. Springer-Verlag, 1990. [pagg. 330, 335]

Bibliografia   299

[29]

Don Davis, Ross Ihaka e Philip Fenstermacher. Cryptographic Randomness from Air
Turbulence in Disk Drives. In Desmedt [31], pagg. 114 – 120. [pag. 138]

[30]

Bert den Boer e Antoon Bosselaers. Collisions for the compression function of MD5. In
Helleseth [61], pagg. 293 – 304. [pag. 81]

[31]

Yvo G. Desmedt, a cura di. Advances in Cryptology – CRYPTO ’94, volume 839 di Lecture
Notes in Computer Science. Springer-Verlag, 1994. [pagg. 327, 330]

[32]

Giovanni Di Crescenzo, Niels Ferguson, Russel Impagliazzo e Markus Jakobsson. How to
Forget a Secret. In Christoph Meinel e Sophie Tison, a cura di, STACS 99, volume 1563
di Lecture Notes in Computer Science, pagg. 500 – 509. Springer-Verlag, 1999. [pag. 127]

[33]

Whitfield Diffie e Martin E. Hellman. New Directions in Cryptography. IEEE Transactions
on Information Theory, IT-22(6):644 – 654, Novembre 1976. [pag. 181]

[34]

Whitfield Diffie, Paul C.Van Oorschot e Michael J.Wiener.Authentication and Authenticated
Key Exchanges. Designs, Codes and Cryptography, 2(2):107 – 125, 1992. [pag. 228]

[35]

Edsger W. Dijkstra. The Humble Programmer. Communications of the ACM, 15(10):859 –
866, 1972. Pubblicato anche come EWD340, http://www.cs.utexas.edu/users/EWD/ewd03xx/
EWD340.PDF. [pag. 118]

[36]

Hans Dobbertin. Cryptanalysis of MD4. J. Cryptology, 11(4):253 – 271, 1998. [pag. 81]

[37]

Mark Dowd, John McDonald e Justin Schuh. The Art of Software Security Assessment:
Identifying and Preventing Software Vulnerabilities. Addison-Wesley, 2006. [pag. 133]

[38]

Orr Dunkelman, Sebastiaan Indesteege e Nathan Keller. A Differential-Linear Attack on
12-Round Serpent. In Dipanwita Roy Chowdhury,Vincent Rijmen e Abhijit Das, a cura
di, Progress in Cryptology – INDOCRYPT 2008, volume 5365 di Lecture Notes in Computer
Science, pagg. 308 – 321. Springer-Verlag, 2008. [pag. 56]

[39]

Stephen R. Dussé e Burton S. Kaliski Jr. A Cryptographic Library for the Motorola
DSP56000. In Damgård [28], pagg. 230 – 244. [pag. 249]

[40]

Morris Dworkin. Recommendation for Block Cipher Modes of Operation – Methods and
Techniques. National Institute of Standards and Technology, December 2001. Disponibile
presso http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf. [pag. 70]

[41]

Morris Dworkin. Recommendation for Block Cipher Modes of Operation: The CCM Mode for
Authentication and Confidentiality. National Institute of Standards and Technology, maggio
2004. Disponibile presso http://csrc.nist.gov/publications/nistpubs/800-38C/SP800-38C.
pdf. [pagg. 71, 112]

[42]

Morris Dworkin. Recommendation for Block Cipher Modes of Opera- tion:The CMAC Mode for
Authentication. National Institute of Standards and Technology, maggio 2005. Disponibile
presso http://csrc.nist.gov/publications/nistpubs/800-38B/SP_800-38B.pdf. [pag. 93]

[43]

Morris Dworkin. Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode
(GCM) and GMAC. National Institute of Standards and Technology, novembre 2007.
Disponibile presso http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf.
[pagg. 71, 94, 113]

[44]

Electronic Frontier Foundation. Cracking DES: Secrets of Encryption Research,Wiretap Politics
& Chip Design. O’Reilly, 1998. [pag. 53]

[45]

Carl Ellison. Improvements on Conventional PKI Wisdom. In Sean Smith, a cura di, 1st
Annual PKI Research Workshop – Proceedings, pagg. 165 – 175, 2002. Disponibile presso
http://www.cs.dartmouth.edu/~pki02/Ellison/. [pagg. 285, 286]

300   Bibliografia

[46]

Jan-Hendrik Evertse ed Eugène van Heyst.Which New RSA-Signatures Can Be Computed
from Certain Given RSA-Signatures? J. Cryptology, 5(1):41 – 52, 1992. [pag. 201]

[47]

H. Feistel,W.A. Notz e J.L. Smith. Some Cryptographic Techniques for Machine-to-Machine
Data Communications. Proceedings of the IEEE, 63(11):1545 – 1554, 1975. [pag. 52]

[48]

Niels Ferguson. Authentication weaknesses in GCM. Public Comments to NIST, 2005.
Vedi http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/comments/CWC-GCM/Ferguson2.pdf.
[pag. 95]

[49]

Niels Ferguson, John Kelsey, Stefan Lucks, Bruce Schneier, Mike Stay, David Wagner e
Doug Whiting. Improved Cryptanalysis of Rijndael. In Bruce Schneier, a cura di, Fast
Software Encryption, 7th International Workshop, FSE 2000, volume 1978 di Lecture Notes in
Computer Science, pagg. 213 – 230. Springer-Verlag, 2000.Vedi anche http://www.schneier.
com/paper-rijndael.html. [pag. 55]

[50]

Niels Ferguson, John Kelsey, Bruce Schneier e Doug Whiting. A Twofish Retreat: RelatedKey Attacks Against Reduced-Round Twofish. Twofish Technical Report 6, Counterpane
Systems, febbraio 2000.Vedi http://www.schneier.com/paper-twofish-related.html. [pag. 45]

[51]

Niels Ferguson and Bruce Schneier. A Cryptographic Evaluation of IPsec, 1999. Vedi
http://www.schneier.com/paper-ipsec.html. [pagg. 104, 321]

[52]

Scott Fluhrer, Itsik Mantin e Adi Shamir. Weaknesses in the Key Schedule Algorithm of
RC4. In Serge Vaudenay e Amr M. Youssef, a cura di, Selected Areas in Cryptography, 8th
Annual International Workshop, SAC 2001, volume 2259 di Lecture Notes in Computer Science.
Springer- Verlag, 2001. [pag. 324]

[53]

Alan O. Freier, Philip Karlton e Paul C. Kocher. The SSL Protocol,Version 3.0. Internet
draft, Transport Layer Security Working Group, 18 novembre 1996. Disponibile presso
http://www.potaroo.net/ietf/idref/draft-freier-ssl-version3/. [pag. 320]

[54]

Ian Goldberg e David Wagner. Randomness and the Netscape Browser. Dr. Dobb’s Journal,
pagg. 66 – 70, gennaio 1996. Disponibile presso http://www.cs.berkeley.edu/~daw/papers/
ddj-netscape.html. [pag. 137]

[55]

Oded Goldreich. Foundations of Cryptography:Volume 1, Basic Tools. Cambridge University
Press, 2001. Disponibile anche presso http://www.wisdom.weizmann.ac.il/~oded/foc-book.
html. [pag. 18]

[56]

Oded Goldreich. Foundations of Cryptography: Volume 2, Basic Applications. Cambridge
University Press, 2001. Disponibile anche presso http://www.wisdom.weizmann.ac.il/~oded/
foc-book.html. [pag. 18]

[57]

Peter Gutmann. Secure Deletion of Data from Magnetic and Solid-State Memory. In
USENIX Security Symposium Proceedings, 1996. Disponibile presso http://www.cs.auckland.
ac.nz/~pgut001/pubs/secure_del.html. [pagg. 125, 312]

[58]

Peter Gutmann. X.509 Style Guide, ottobre 2000. Disponibile presso

http://www.

cs.auckland.ac.nz/~pgut001/pubs/x509guide.txt. [pag. 279]

[59]

J. Alex Halderman, Seth D. Schoen, Nadia Heninger, William Clarkson, William Paul,
Joseph A. Calandrino, Ariel J. Feldman, Jacob Appelbaum e Edward W. Felten. Lest We
Remember: Cold Boot Attacks on Encryption Keys. In USENIX Security Symposium
Proceedings, pagg. 45 – 60, 2008. [pagg. 125, 126]

[60]

D. Harkins e D. Carrel. The Internet Key Exchange (IKE). RFC 2409, novembre 1998.
[pagg. 191, 192]

Bibliografia   301

[61]

Tor Helleseth, a cura di. Advances in Cryptology – EUROCRYPT ’93, volume 765 di Lecture
Notes in Computer Science. Springer-Verlag, 1993. [pagg. 328, 330]

[62]

Michael Howard e Steve Lipner. The Security Development Lifecycle. Microsoft Press, 2006.
[pag. 133]

[63]

Intel. Intel 82802 Firmware Hub: Random Number Generator, Programmer’s Reference Manual,
dicembre 1999. Disponibile presso il sito web di Intel. [pag. 139]

[64]

International Telecommunication Union. X.680-X.683: Abstract Syntax Notation One
(ASN.1), X.690-X.693: ASN.1 encoding rules, 2002. [pag. 220]

[65]

Jakob Jonsson. On the Security of CTR + CBC-MAC. In Selected Areas in Cryptography, 9th
Annual International Workshop, SAC 2002, 2002.Vedi http://csrc.nist.gov/groups/ST/toolkit/
BCM/documents/proposedmodes/ccm/ccm-ad1.pdf. [pag. 112]

[66]

Robert R. Jueneman. Analysis of Certain Aspects of Output Feedback Mode. In David
Chaum, Ronald L. Rivest e Alan T. Sherman, a cura di, Advances in Cryptology, Proceedings
of Crypto 82, pagg. 99 – 128. Plenum Press, 1982. [pag. 69]

[67]

David Kahn. The Codebreakers,The Story of Secret Writing. Macmillan Publishing Co., New
York, 1967. [pag. 18]

[68]

Jonathan Katz e Yehuda Lindell. Introduction to Modern Cryptography: Principles and Protocols.
Chapman & Hall/CRC, 2007. [pag. 18]

[69]

John Kelsey, Bruce Schneier e Niels Ferguson.Yarrow-160: Notes on the Design and Analysis
of theYarrow Cryptographic Pseudorandom Number Generator. In Howard Heys e Carlisle
Adams, a cura di, Selected Areas in Cryptography, 6th Annual International Workshop, SAC ’99,
volume 1758 di Lecture Notes in Computer Science. Springer-Verlag, 1999. [pag. 141]

[70]

John Kelsey, Bruce Schneier e David Wagner. Key-Schedule Cryptanalysis of IDEA, G-DES,
GOST, SAFER e Triple-DES. In Koblitz [76], pagg. 237 – 251. [pag. 45]

[71]

John Kelsey, Bruce Schneier, David Wagner e Chris Hall. Cryptanalytic Attacks on
Pseudorandom Number Generators. In Serge Vaudenay, a cura di, Fast Software Encryption,
5th International Workshop, FSE’98, volume 1372 di Lecture Notes in Computer Science, pagg.
168 – 188. Springer-Verlag, 1998. [pag. 141]

[72]

John Kelsey, Bruce Schneier, David Wagner e Chris Hall. Side Channel Cryptanalysis of
Product Ciphers. Journal of Computer Security, 8(2 – 3):141 – 158, 2000.Vedi anche http://
www.schneier.com/paper-side-channel.html. [pag. 132]

[73]

S. Kent e R.Atkinson. Security Architecture for the Internet Protocol. RFC 2401, novembre
1998. [pag. 111]

[74]

Lars R. Knudsen e Vincent Rijmen. Two Rights Sometimes Make a Wrong. In Workshop
on Selected Areas in Cryptography (SAC ’97), pages 213 – 223, 1997. [pag. 44]

[75]

Donald E. Knuth. Seminumerical Algorithms, volume 2 of The Art of Computer Programming.
Addison-Wesley, 1981. [pagg. 140, 170, 173, 243]

[76]

Neal Koblitz, a cura di. Advances in Cryptology – CRYPTO ’96, volume 1109 di Lecture
Notes in Computer Science. Springer-Verlag, 1996. [pagg. 327, 333, 334]

[77]

Paul Kocher, Joshua Jaffe e Benjamin Jun. Differential Power Analysis. In Michael Wiener,
a cura di, Advances in Cryptology – CRYPTO ’99, volume 1666 di Lecture Notes in Computer
Science, pagg. 388 – 397. Springer-Verlag, 1999. [pag. 132]

[78]

Paul C. Kocher. Timing Attacks on Implementations of Diffie-Hellman, RSA, DSS, and
Other Systems. In Koblitz [76], pagg. 104 – 113. [pagg. 251, 252]

302   Bibliografia

[79]

J. Kohl e C. Neuman. The Kerberos Network Authentication Service (V5). RFC 1510,
settembre 1993. [pag. 270]

[80]

Tadayoshi Kohno, JohnViega e Doug Whiting. CWC:A High-Performance Conventional
Authenticated Encryption Mode. In Bimal Roy e Willi Meier, a cura di, Fast Software
Encryption, 11th International Workshop, FSE 2004, volume 3017 di Lecture Notes in
Computer Science, pagg. 408 – 426. Springer-Verlag, 2004. [pag. 112]

[81]

H. Krawczyk, M. Bellare e R. Canetti. HMAC: Keyed-Hashing for Message Authentication.
RFC 2104, febbraio 1997. [pag. 93]

[82]

Hugo Krawczyk. The Order of Encryption and Authentication for Protecting
Communications (or: How Secure is SSL?). In Joe Kilian, a cura di, Advances in Cryptology
– CRYPTO 2001, volume 2139 di Lecture Notes in Computer Science, pagg. 310 – 331.
Springer-Verlag, 2001. [pag. 102]

[83]

Xuejia Lai, James L. Massey e Sean Murphy. Markov Ciphers and Differential Cryptanalysis.
In D.W. Davies, a cura di, Advances in Cryptology – EUROCRYPT ’91, volume 547 di
Lecture Notes in Computer Science, pagg. 17 – 38. Springer-Verlag, 1991. [pag. 250]

[84]

Xuejia Lai e James L. Massey.A Proposal for a New Block Encryption Standard. In Damgård
[28], pagg. 389 – 404. [pag. 250]

[85]

Arjen K. Lenstra e Eric R. Verheul. Selecting Cryptographic Key Sizes. J. Cryptology,
14(4):255 – 293, agosto 2001. [pagg. 36, 189]

[86]

Michael Luby and Charles Rackoff. How to Construct Pseudorandom Permutations from
Pseudorandom Functions. SIAM J. Computation, 17(2), aprile 1988. [pag. 46]

[87]

T. Matsumoto, H. Matsumoto, K.Yamada e S. Hoshino. Impact of Artificial “Gummy” Fingers
on Fingerprint Systems. In Proceedings of SPIE, Vol #4677, Optical Security and Counterfeit
Deterrence Techniques IV, 2002.Vedi anche http://cryptome.org/gummy.htm. [pag. 308]

[88]

Gary McGraw. Software Security: Building Security In. Addison-Wesley, 2006.
[pag. 133]

[89]

A.J. Menezes e S.A. Vanstone, a cura di. Advances in Cryptology – CRYPTO ’90, volume
537 di Lecture Notes in Computer Science. Springer-Verlag, 1990. [pagg. 329, 336]

[90]

Alfred J. Menezes, Paul C. van Oorschot e Scott A. Vanstone. Handbook of Applied
Cryptography. CRC Press, 1996. Disponibile anche presso http://www.cacr.math.uwaterloo.
ca/hac/. [pagg. 18, 243]

[91]

D. Mills. Simple Network Time Protocol (SNTP) Version 4. RFC 2030, ottobre 1996.
[pag. 264]

[92]

David L. Mills. Network Time Protocol (Version 3). RFC 1305, marzo 1992. [pag. 264]

[93]

P. Montgomery. Modular Multiplication without Trial Division. Mathematics of Computation,
44(170):519 – 521, 1985. [pag. 249]

[94]

Moni Naor e Omer Reingold. On the Construction of Pseudorandom Permutations: LubyRackoff Revisited. J. Cryptology, 12(1):29 – 66, 1999. [pag. 46]

[95]

National Institute of Standards and Technology. DES Modes of Operation, dicembre 2, 1980.
FIPS PUB 81, disponibile presso http://www.itl.nist.gov/fipspubs/fip81.htm. [pag. 70]

[96]

National Institute of Standards and Technology. Data Encryption Standard (DES), dicembre
30, 1993. FIPS PUB 46-2, disponibile presso http://www.itl.nist.gov/fipspubs/fip46-2.htm.
[pagg. 51, 52]

Bibliografia   303

[97]

National Institute of Standards and Technology. Secure Hash Standard, aprile 17, 1995. FIPS
PUB 180-1, disponibile presso http://www.digistamp.com/reference/fip180-1.pdf. [pag. 82]

[98]

National Institute of Standards and Technology. AES Round 1 Technical Evaluation, CD-1:
Documentation, agosto 1998. [pagg. 54, 327, 329, 337]

[99]

National Institute of Standards and Technology. Data Encryption Standard (DES), 1999. FIPS
PUB 46-3, disponibile presso http://csrc.nist.gov/publications/fips/fips46-3/fips46-3.
pdf. [pag. 51]

[100] National Institute of Standards and Technology. Proc. 3rd AES candidate conference, aprile
2000. [pag. 54]
[101] National Institute of Standards and Technology. Secure Hash Standard (draft), 2008. FIPS
PUB 180-3, disponibile presso http://csrc.nist.gov/publications/fips/fips180-3/fips1803_final.pdf. [pag. 82]
[102] Roger M. Needham e Michael D. Schroeder. Using Encryption for Authentication in
Large Networks of Computers. Communications of the ACM, 21(12):993 – 999, dicembre
1978. [pag. 270]
[103] Bart Preneel e Paul C. van Oorschot. On the Security of Two MAC Algorithms. In Ueli
Maurer, a cura di, Advances in Cryptology – EURO- CRYPT ’96, volume 1070 di Lecture
Notes in Computer Science, pagg. 19 – 32. Springer-Verlag, 1996. [pag. 93]
[104] R. Rivest.The MD5 Message-Digest Algorithm. RFC 1321, aprile 1992. [pag. 81]
[105] Ronald Rivest,Adi Shamir e Leonard Adleman.A Method for Obtaining Digital Signatures
and Public-Key Cryptosystems. Communications of the ACM, 21:120 – 126, febbraio 1978.
[pag. 195]
[106] Ronald L. Rivest. The MD4 Message Digest Algorithm. In Menezes and Vanstone [89],
pagg. 303 – 311. [pag. 81]
[107] Ronald L. Rivest. The RC5 Encryption Algorithm. In B. Preneel, a cura di, Fast Software
Encryption, Second International Workshop, FSE’94, volume 1008 di Lecture Notes in Computer
Science, pagg. 86 – 96. Springer-Verlag, 1995. [pag. 251]
[108] Ronald L. Rivest, M.J.B. Robshaw, R. Sidney e Y.L. Yin. The RC6 Block Cipher. In
National Institute of Standards and Technology [98]. Vedi http://people.csail.mit.edu/
rivest/Rc6.pdf. [pagg. 58, 251]
[109] Phillip Rogaway, Mihir Bellare, John Black e Ted Krovetz. OCB: A Block-Cipher Mode of
Operation for Efficient Authenticated Encryption. In Eighth ACM Conference on Computer
and Communications Security (CCS-8), pagg. 196 – 205. ACM, ACM Press, 2001. [pag. 112]
[110] RSA Laboratories. PKCS #1 v2.1: RSA Cryptography Standard, gennaio 2001. Disponibile
presso http://www.rsa.com/rsalabs/node.asp?id=2124. [pag. 206]
[111] Bruce Schneier. Applied Cryptography, Protocols,Algorithms ece Code in C. John Wiley & Sons,
Inc., 1994. [pag. 323]
[112] Bruce Schneier. Applied Cryptography, Second Edition, Protocols, Algorithms, and Source Code
in C. John Wiley & Sons, Inc., 1996. [pagg. 18, 323]
[113] Bruce Schneier. Attack Trees. Dr. Dobb’s Journal, 1999. Disponibile anche presso
www.schneier.com/paper-attacktrees-ddj-ft.html. [pag. 5]

http://

[114] Bruce Schneier. Secrets and Lies: Digital Security in a Networked World. John Wiley & Sons,
Inc., 2000. [pagg. 16, 18]

304   Bibliografia

[115] Bruce Schneier, John Kelsey, Doug Whiting, David Wagner, Chris Hall e Niels Ferguson.
The Twofish Encryption Algorithm, A 128-Bit Block Cipher. John Wiley & Sons, Inc., 1999.
[pagg. 45, 57]
[116] Dr. Seuss. Horton Hatches the Egg. Random House, 1940. [pag. 97]
[117] Adi Shamir. How to Share a Secret. Communications of the ACM, 22(11):612 – 613, 1979.
[pag. 310]
[118] C.E. Shannon. A Mathematical Theory of Communication. The Bell Systems Technical
Journal, 27:370 – 423 e 623 – 656, luglio e ottobre 1948.Vedi http://cm.bell-labs.com/cm/
ms/what/shannonday/paper.html. [pag. 137]
[119] Victor Shoup, a cura di. Advances in Cryptology – CRYPTO 2005, volume 3621 di Lecture
Notes in Computer Science. Springer-Verlag, 2005. [pagg. 329, 338]
[120] Simon Singh. The Code Book: The Science of Secrecy from Ancient Egypt to Quantum
Cryptography. Anchor, 2000. [pag. 18]
[121] David Wagner, Niels Ferguson e Bruce Schneier. Cryptanalysis of FROG. In Proc. 2nd
AES candidate conference, pagg. 175 – 181. National Institute of Standards and Technology,
marzo 1999. [pag. 44]
[122] David Wagner e Bruce Schneier. Analysis of the SSL 3.0 protocol. In Proceedings of the Second
USENIX Workshop on Electronic Commerce, pagg. 29 – 40, novembre 1996. Versione rivista
disponibile presso http://www.schneier.com/paper-ssl.html. [pag. 97]
[123] Xiaoyun Wang, Yiqun Lisa Yin e Hongbo Yu. Finding Collisions in the Full SHA-1. In
Shoup [119], pagg. 17 – 36. [pag. 82]
[124] Xiaoyun Wang e Hongbo Yu. How to Break MD5 and Other Hash Functions. In Ronald
Cramer, a cura di, Advances in Cryptology – EURO- CRYPT 2005, volume 3494 di Lecture
Notes in Computer Science, pages 19 – 35. Springer-Verlag, 2005. [pag. 81]
[125] Mark N.Wegman e J. Lawrence Carter. New Hash Functions and Their Use in Authentication
and Set Equality. J. Computer and System Sciences, 22(3):265 – 279, 1981. [pagg. 94, 112]
[126] Doug Whiting, Russ Housley, and Niels Ferguson. Counter with CBC-MAC (CCM),
giugno 2002.Vedi http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/ccm/
ccm.pdf. [pag. 112]
[127] Michael J. Wiener. Cryptanalysis of short RSA secret exponents. IEEE Transactions on
Information Theory, 36(3):553 – 558, maggio 1990. [pag. 202]
[128] Robert S.Winternitz. Producing a One-way Hash Function from DES. In David Chaum,
a cura di, Advances in Cryptology, Proceedings of Crypto 83, pagg. 203 – 207. Plenum Press,
1983. [pag. 45]
[129] Thomas Wu. The Secure Remote Password Protocol. In Proceedings of the 1998 Network and
Distributed System Security (NDSS’98) Symposium, marzo 1998. [pag. 241]
[130] Phil Zimmermann e Jon Callas.The Evolution of PGP’s Web of Trust. In Andy Oram and
John Viega, a cura di, Beautiful Security, pagg. 107 – 130. O’Reilly, 2009. [pag. 289]

Indice analitico

A
Accessi indesiderati, 118
ACL (Access Control List), 258
AES, 291
AES (Advanced Encryption Standard), 49
cicli, 49
Aggiornamento di file di seme, 144
Albero di attacco, 5
Algoritmo di Euclide esteso, 156–157
Analisi del traffico, 95
Anello, 178
Asserzioni, 121
Atomicità degli aggiornamenti del file
system, 145–146
Attacchi, 28–31
generici, 12–13
Attacchi a canale laterale, 54, 123–124,
225–227
Attacchi con replica, 202
Attacchi del compleanno, 31
Attacchi di temporizzazione, 54
Attacchi meet-in-the-middle, 32
Attacco con ricerca esaustiva, 33
Attacco con solo testo cifrato (ciphertextonly), 28
Attacco con testo cifrato scelto, 30
Attacco con testo in chiaro noto (knownplaintext), 29
Attacco con testo in chiaro scelto, 29
Attacco del compleanno
e attacco meet-in-the-middle, 32
Attacco di parità, 45

Attacco discriminante, 30
Attacco man-in-the-middle, 168
Attacco meet-in-the-middle
e attacco del compleanno, 32
Attacco SYN flood, 229
Attack tree, 5
Autenticazione, 23–25
convenzione, 208
e cifratura insieme, 64
Autenticazione anticipata, 96
principio di Horton, 97
Autorità di certificazione, 27
Autorità di registrazione separata, 253
Autorizzazione diretta, 259
Autorizzazione indiretta, 258

B
Backup, 145
e macchine virtuali, 145
Banca elettronica, 250
Base, 161
Biometria, 279–280
BIOS, 121
Black box, 12
Brevetti, 287–292, 292
Buffer overflow, 4, 122

C
CA (Certificate Authority), 249
CA (Certification Authority), 27
Calcoli aritmetici su interi grandi, 219

306   Indice analitico

Calcolo di potenze, 163
Calcolo modulare, 153–160
addizione, 154
campi finiti, 154
gruppi, 154
moltiplicazione, 154
sottrazione, 154
Calcolo modulo 2, 158
Campi finiti, 154
Canale sicuro, 93–106
alternative, 105
autenticazione, 99
chiave, 94
cifratura, 99
dettagli di progettazione, 100
formato del frame, 100
inizializzazione, 101
invio di un messaggio, 102
numerazione dei messaggi, 98
progettazione, 98–100
proprietà, 93
ricezione di un messaggio, 103
sequenza, 94
sicurezza, 95
Cancellazione dei segreti, 282
Cancellazione dello stato, 112
Casualità, 128–130
CBC (Cipher Block Chaining), 59
CBC-MAC, 84
CCM, 105
Ceiling, 58
Certificati
formato, 267
Certificati multilivello, 251
Certificato, 27
Certificato autocertificante, 268
Certificato di root, 268
Chiamante, 93
Chiamato, 93
Chiave di root, 268
Chiave di sessione, 94
Chiave privata, 183
Chiave pubblica, 25
Chiave segreta, 24

Chiave simmetrica
dimensioni, 172
Chiavi
certificazione, 269
creazione, 269
distribuzione, 269
scadenza, 269
uso attivo, 269
uso passivo, 269
usura, 270
Chip RTC, 235
Cifrari a blocchi, 39–56
AES (Advanced Encryption Standard),
49
attacchi, 40
definizione, 39
DES (Data Encryption Standard), 46
ideali, 41–42
interfaccia, 44
lunghezza della chiave, 54
modalità, 57–70
reali, 46–55
riempimento, 58
scelta, 53
scelta della modalità, 64
Serpent, 51
sicurezza, 42–45
Twofish, 52–53
Cifrario a flusso, 62
Cifratura, 21–23, 187–190
e autenticazione insieme, 64
Cifratura a chiave asimmetrica, 26
Cifratura a chiave pubblica, 25–26
Cifratura anticipata, 96
Circuiti di carte di credito, 251
CMAC, 84
Codice di autenticazione del messaggio, 24
Codici di autenticazione dei messaggi,
83–92
azione, 83
ideali, 84
scelta, 88
sicurezza, 84
utilizzo, 89

Indice analitico   307

Codifica e analisi del messaggio, 200
Compromissione della chiave, 215
Connessione sicura, 245
Conservazione dei segreti, 273–284
Convenzione di autenticazione, 208
Core dump, 118
Costrutto di Feistel, 48
Creazione di software sicuro, 111–112
Crittografia
arte e scienza, 3
complessità, 34
difficoltà, 11
obiettivi, 220
prestazioni, 34
ruolo, 4
CRL (Certificate Revocation List), 262
CRT (Chinese Remainder Theorem), 178
CTR (CounTeR), 63
CWC, 105

D
Dati casuali
e PRNG, 130
generazione, 127–148
problemi, 129
Dati pseudocasuali, 129
Davies-Meyer, 41
DES (Data Encryption Standard), 46
cicli, 47
Difesa in profondità, 6
Diffie-Hellman, 165–176
e RSA, 177
gruppi, 166
numeri primi, 166
Digest, 71
Discriminatore, 42
Dispositivi sicuri, 278
Divisibilità
e numeri primi, 149–151
Divisore, 149
DoS (Denial of Service), 97
DRAM (RAM dinamica), 117
DRM (Digital Rights Management), 12

E
Eavesdropping, 21
ECB (Electronic CodeBook), 59
ECC (Error-Correcting Code), 119
Elenco dei certificati revocati, 262
Entropia, 127
sorgenti, 136
European Committee for Standardization
(CEN), 287

F
FEAL, 50
Fiducia, 194
etica, 194
legge, 194
MAD, 194
minaccia fisica, 194
nei protocolli crittografici, 197
reputazione, 194
File di swapping, 114
Fingerprint, 71
Firme, 190
Firme cieche, 227
Firme digitali, 26–27
con RSA, 182
Formula di Garner, 178
Fortuna, 131, 190
accumulatore, 136
generatore, 132
inizializzazione, 133, 141
velocità del generatore, 136
forward secrecy, 246
Fuga di informazioni, 65–68
gestione, 67
Funzione black-box, 42
Funzione di cifratura, 22
Funzioni di hash, 71–82
collisione dei messaggi parziali, 77
definizione, 71
estensione della lunghezza, 77
ideali, 73
iterative, 79
MD5, 75
non reversibilità, 72

308   Indice analitico

reali, 74
resistenza alle collisioni, 72
scelta, 81
sicurezza, 72
troncamento dell’output, 80
vulnerabilità, 77

G
Garbage collection, 113
Garner, formula, 178
Generazione di blocchi, 134
Generazione di dati casuali, 127–148
Generazione di numeri primi piccoli,
151–153
Gestione del file di seme, 143
Gestione delle chiavi, 243
concetti di base, 244
scelta del meccanismo, 266
GMAC, 87
Gruppi, 154, 166
Gruppo moltiplicativo modulo p, 155
Guessing entropy, 131

H

Infrastruttura a chiave pubblica, 27
Institute of Electrical and Electronics
Engineers (IEEE), 287
Integrità dei dati, 119
Interfaccia utente sicura, 279
International Organization for
Standardization (ISO), 287
Internet Engineering Task Force (IETF),
287
Invio di un messaggio, 102
IV (Initialization Vector), 59
a contatore, 60
casuale, 60
fisso, 60
generato da nonce, 61

K
Kerberos, 228, 244
Kerckhoff, principio, 22–23
Key server, 243
Key stream, 62

L

HMAC, 87
Horton, principio, 89

Linguaggio per i permessi, 267
Livelli di sicurezza, 33
Livello di trasporto, 198

I

M

IDEA, 50, 226
Identificatore dell’istanza del protocollo,
228
Identificatore del messaggio, 228
Identificatore del protocollo, 228
Iimplementazione, 219–230
IKE (Internet Key Exchange), 174
Implementazione, 107–124
sistema operativo, 108
specifiche, 109
test, 110
Impostazione di una chiave, 246
Impronta digitale, 71
Incentivi, 195
Information leakage, 31

MAC (Message Authentication Code), 24,
83
attacchi, 84
azione, 83
ideali, 84
scelta, 88
sicurezza, 84
utilizzo, 89
MAD (Mutually Assured Destruction), 194
Mantenimento dei dati in memoria, 116
MARS, 53, 226
Massimo comun divisore, 156
MD5, 75
calcolo, 75
Memoria cache, 115

Indice analitico   309

Message digest, 71
Messaggi
analisi, 200
codifica, 200
Metodo di Montgomery, 224
Minimo comune multiplo, 156
Modalità dei cifrari a blocchi, 57–70
CBC (Cipher Block Chaining), 59
CTR (CounTeR), 63
ECB (Electronic CodeBook), 59
OFB (Output FeedBack), 62–64
riempimento, 58
scelta, 64
Modalità di cifratura a blocchi, 40
Modello delle minacce, 9–11
Moltiplicazione modulare
più rapida, 224
Moltiplicazione modulo n, 181
Montgomery, metodo, 224

N
Needham-Schroeder, protocollo, 244
Negoziazione della chiave, 205–218
a partire da una password, 218
NIST (National Institute of Standards and
Technology), 49
Nonce, 61
NTP, 238
Numeri primi, 149–164
calcolo modulare, 153–160
definizione, 149
e divisibilità, 149–151
generazione di piccoli, 151–153
grandi, 158–160
sicuri, 170
teorema (Euclide), 151

O
OCSP (Online Certificate Status
Protocol), 263
OFB (Output FeedBack), 62–64
Orario di riferimento, 240
Ordinamento dei messaggi, 104
Ordine di autenticazione, 96

Orologio, 233–242
creazione di uno affidabile, 237–238
fermare, 236
impieghi, 233
monotonicità, 234
pericoli per la sicurezza, 235
portare avanti, 237
portare indietro, 235
valore unico, 234

P
Padding, 58
Paradosso di Einstein-Podolsky-Rosen,
129
Paranoia professionale, 7
esercizi, 16
parlare degli attacchi, 8
vantaggi, 8
Parità di una permutazione, 44–46
Parte intera superiore, 58
Passphrase, 275
Password, 274
allungamento, 275
sale, 275
Permutazioni
dispari, 45
pari, 45
PKI (Public Key Infrastructure), 27
PKI (Public-Key Infrastructure), 249–254
autorità, 257
autorizzazione diretta, 259
autorizzazione indiretta, 258
considerazioni pratiche, 267–272
e server di chiavi a confronto, 265
fiducia, 257
formato del certificato, 267
nomi, 255
revoca, 262
universale, 250
Pool, 137
distribuzione degli eventi, 139
Preimmagine, 73
Principio di Horton, 89
autenticazione anticipata, 97

310   Indice analitico

Principio di Kerckhoff, 22–23, 40
PRNG, 123
Fortuna, 131
modelli di attacco, 130
PRNG (PseudoRandom Number
Generator), 129
Probabilità di una collisione, 66
Problema dello stato che non cambia, 238
Progettazione di un canale sicuro, 98–100
Proprietà dell’elemento più debole, 5–6
Protocolli crittografici, 193–204
attività, 198
codifica e analisi del messaggio, 200
compatibilità con versioni precedenti,
207
complessità, 217
complessità computazionale, 215–216
errori, 201
fiducia, 194
identità del messaggio, 199
identità del protocollo, 199
implementazione, 227
livello di trasporto, 198
messaggi, 198
rischio, 195
ruoli, 193
stati di esecuzione, 200
su un canale sicuro, 228
trucchi di ottimizzazione, 216
visioni, 213
Protocollo DH, 166, 206
originale, 167
parametri, 171
regole pratiche, 173
trappole, 169
verifica dei calcoli, 223
Protocollo di negoziazione della chiave,
205–218
Protocollo Needham-Schroeder, 244

Q
Qualità del codice, 120–123
asserzioni, 121

modularità, 120
semplicità, 120

R
RA (Registration Authority), 253
RC6, 53
Revisioni della sicurezza, 17
Revoca, 262
obbligatoria, 264
Ricezione di un messaggio, 103, 228
Rijndael, 49
Rinnovo della chiave, 246
Rischio di perdita dei dati, 281
RNG (Random Number Generator), 127
RSA, 177–192, 216
chiave privata, 183
cifratura, 187–190
definizione, 182
e Diffie-Hellman, 177
esponenti pubblici, 182
firme, 190
firme digitali, 182
generazione di chiavi, 185
pericoli, 186–187
verifica della cifratura, 224
verifica delle firme, 224
RTC (Real-Time Clock), 235

S
Sale, 275
S-box, 47
Scadenza delle chiavi, 252
Scadenza rapida, 263
Scelta di elementi casuali, 147
Scrittura di file di seme, 144
Secret sharing, 281
Segreti
cancellazione, 282
conservazione, 273–284
imparati a memoria, 274
scritti sulla carta, 282
unità a disco, 273
Seme, 129, 134

Indice analitico   311

aggiornamento di file, 144
gestione dei file, 143
scrittura di file, 144
Sensori di una raffineria, 251
Serpent, 51
Server di chiavi, 243–248
e PKI a confronto, 265
SET, 10
SHA, 74
SHA-1, 75
SHA-2, 76
SHA-3, 72
SHA-256, 105
Sicurezza
e funzionalità, 15
e prestazioni, 13
e sistemi in evoluzione, 15
livelli, 33
Simulazione Monte Carlo, 133
Sistema operativo, 108
Sistemi di credenziali, 260
SNTP, 238
Sorgenti di entropia, 136
Sottogruppi, 166
Specifiche, 109
progetto di implementazione, 109
requisiti, 109
specifica funzionale, 109
SRAM (RAM statica), 116
SSL, 290
SSO (Single Sign-On), 280
Standard, 287–292
funzionalità, 289
leggibilità, 289
processo di creazione, 287
sicurezza, 289
STS (Station-to-Station), 206
Supporti a stato solido, 284
Supporti di archiviazione portatili, 277
Supporti magnetici, 283
SYN flood, 229

T
Tempo di esecuzione del passaggio di
eventi, 140
Teorema cinese del resto, 178
generalizzazioni, 179–180
impieghi, 180
Teorema di Euclide sui numeri primi, 151
Teorema fondamentale dell’aritmetica, 151
Test, 110, 122
Test di primalità, 160–163
di Fermat, 162
di Rabin-Miller, 162
Testo cifrato, 22
Testo in chiaro, 22
Timeout, 229
Time server, 238
Transazioni in tempo reale, 234
Triple DES, 46, 54
Twofish, 41, 52–53
struttura, 52

U
UTC, 240

V
Valori woop, 221
Verifica di certificati online, 263
Vettore di inizializzazione, 59
Vita di una chiave, 269
VPN (Virtual Private Network), 250

W
WEP (Wired Equivalent Privacy), 293
Wooping, 221–223

X
X.509, 253

Licenza
edgt-49-PTAZIQ6MWREQ4XHYFQUWASU6UTYIKD7R-USPWS1YTZ78HC2WFZYAGMBOLAN
rilasciata il 09 settembre 2015 a amanda bourdillon su

Sponsor Documents

Or use your account on DocShare.tips

Hide

Forgot your password?

Or register your new account on DocShare.tips

Hide

Lost your password? Please enter your email address. You will receive a link to create a new password.

Back to log-in

Close