Crittografia a chiave asimmetrica
In questa lezione esamineremo la crittografia a chiave asimmetrica, che costituisce la base di molte interazioni di rete sicure oggi.
Alla fine della lezione avremo trattato:
- Cos'è la crittografia a chiave asimmetrica
- L'utilizzo della crittografia a chiave asimmetrica, inclusi scambio di chiavi e firme digitali
- La sicurezza della crittografia a chiave asimmetrica in generale
- Ulteriori dettagli sugli algoritmi RSA, DSA ed Elliptic Curves e sulla loro sicurezza
- Alcuni esempi di codice Python che mostrano come funzionano gli algoritmi in pratica
- Le minacce a questi algoritmi provenienti sia da computer classici che quantistici
Introduzione alla crittografia a chiave asimmetrica​
Come abbiamo imparato nell'ultima lezione, la crittografia a chiave simmetrica è molto veloce ed efficiente per proteggere le informazioni, ma presenta alcune limitazioni:
- All'aumentare del numero di parti che desiderano scambiarsi informazioni sicure, il numero di chiavi necessarie cresce in modo combinatorio. Non fornisce alcun meccanismo per distribuire in modo sicuro queste chiavi tra mittenti e destinatari.
- Non è prevista la non-repudiabilità . Qualsiasi parte è in grado di decifrare o cifrare messaggi senza alcun modo per garantire che un messaggio sia stato ricevuto o da dove provenga.
La soluzione a entrambi questi problemi è fornita dalla crittografia a chiave asimmetrica (AKC), nota anche come crittografia a chiave pubblica (PKC), che costituisce quindi un pilastro della sicurezza digitale moderna.
La crittografia a chiave asimmetrica (AKC) prevede l'uso di una coppia di chiavi — una pubblica, una privata. Le chiavi pubblica e privata sono collegate crittograficamente e tipicamente generate nello stesso momento come coppia di chiavi utilizzando un algoritmo matematico specializzato. La chiave pubblica, come suggerisce il nome, è destinata a essere distribuita liberamente, mentre la chiave privata è tenuta segreta dalla parte che genera la coppia di chiavi. La sicurezza delle comunicazioni che impiegano coppie di chiavi asimmetriche è garantita fintanto che la chiave privata rimane riservata.

Figura 1. Cifratura con chiave asimmetrica
L'AKC offre diverse funzioni utili, come:
- Cifratura e decifratura per garantire la riservatezza delle comunicazioni.
- Firme digitali per garantire autenticità , integrità e non-repudiabilità .
- Scambio di chiavi sicuro per facilitare l'uso successivo di crittosistemi simmetrici.
Nelle applicazioni moderne, l'AKC viene utilizzata principalmente per firme digitali e scambio sicuro di chiavi. In questa lezione, introduciamo queste due funzioni chiave, e poi discutiamo diverse varianti di protocolli crittografici per tali funzioni.
Scambio di chiavi con crittografia a chiave asimmetrica​
Uno dei problemi fondamentali della crittografia è lo scambio di chiavi in modo sicuro. Ad esempio, se due parti vogliono utilizzare la cifratura simmetrica, entrambe devono avere la stessa chiave per cifrare e decifrare i messaggi. Ma come si scambiano la chiave in modo sicuro? La crittografia a chiave asimmetrica affronta questo problema attraverso meccanismi di scambio di chiavi interattivi e non interattivi.
Scambio di chiavi interattivo​
Un protocollo di scambio di chiavi interattivo si riferisce a un metodo in cui due parti collaborano per creare una chiave segreta condivisa su un canale di comunicazione non sicuro. Questa chiave segreta condivisa può poi essere utilizzata per compiti di cifratura e decifratura simmetrica.
Il più noto tra tali protocolli è l'algoritmo Diffie-Hellman (DH), ideato specificamente per facilitare lo scambio di chiavi. In questo protocollo, ogni parte genera una coppia di chiavi (pubblica e privata) e trasmette la propria chiave pubblica. Poi ciascuna parte usa la propria chiave privata e la chiave pubblica dell'altra parte per generare una chiave segreta condivisa. Il DH utilizza i principi dell'aritmetica modulare per garantire che entrambe le parti arrivino alla stessa chiave segreta condivisa anche se ciascuna ha accesso solo alla chiave pubblica dell'altra.
I crittosistemi moderni basati sulla crittografia su curve ellittiche (ECC) estendono questo concetto con lo scambio di chiavi Diffie-Hellman su curve ellittiche (ECDH). L'ECDH funziona in modo simile al DH ma utilizza le proprietà delle curve ellittiche, risultando in un sistema più sicuro ed efficiente.

Figura 2. Protocollo di scambio di chiavi
Scambio di chiavi non interattivo​
A differenza dei protocolli di scambio di chiavi come DH e ECDH, che sono interattivi e richiedono comunicazione bidirezionale per stabilire la chiave simmetrica, l'AKC prevede anche modalità non interattive per stabilire una chiave segreta condivisa. In tali schemi, una parte genera una coppia di chiavi (pubblica e privata) e condivide la chiave pubblica con l'altra parte. Questa seconda parte genera quindi una chiave simmetrica casuale, la cifra con la chiave pubblica ricevuta e la rimanda alla prima parte. La prima parte utilizza la propria chiave privata per decifrare il messaggio ricevuto, ottenendo così la chiave simmetrica condivisa. Questo schema è non interattivo nel senso che la chiave simmetrica è determinata da una sola parte e semplicemente comunicata in modo sicuro all'altra parte in forma cifrata.
Una considerazione importante nello scambio di chiavi non interattivo riguarda la differenza di lunghezza in bit tra la chiave simmetrica che le parti desiderano scambiare e le dimensioni dei messaggi raccomandate nell'AKC. Tipicamente, le chiavi simmetriche moderne hanno una lunghezza compresa tra 128 e 256 bit, mentre i crittosistemi a chiave asimmetrica come RSA lavorano con dimensioni dei messaggi intorno a 1024-4096 bit. Pertanto, quando si utilizza l'AKC per trasmettere una chiave simmetrica, per sicurezza essa deve comunque essere codificata in un messaggio più lungo di 1024-4096 bit. Questo può essere ottenuto tramite due approcci:
-
Scambio di chiavi basato su padding: In questo approccio, la chiave simmetrica più corta (128-256 bit) viene generata per prima e poi uno schema di padding reversibile concordato come OAEP viene utilizzato per incorporarla in un messaggio più lungo (1024-4096 bit). Questo messaggio più lungo viene cifrato tramite AKC e trasmesso come testo cifrato. Il destinatario prima decifra il testo cifrato e poi rimuove il padding per estrarre la chiave simmetrica più corta.
-
Meccanismi di incapsulamento delle chiavi (KEM): Con lo scambio di chiavi basato su KEM, viene prima generato un messaggio in chiaro casuale e lungo (1024-4096 bit), dal quale è possibile estrarre una chiave simmetrica più corta (128-256 bit) utilizzando una funzione di derivazione delle chiavi (KDF) concordata. Il testo in chiaro più lungo viene cifrato tramite AKC e trasmesso al destinatario come testo cifrato. Il destinatario decodifica il testo cifrato usando la propria chiave privata e poi usa la KDF per estrarre la chiave simmetrica più corta (128-256 bit). Crittosistemi diffusi come RSA, con la loro capacità di cifrare direttamente i dati, possono essere utilizzati per implementare i KEM.

Figura 3. Meccanismo di incapsulamento delle chiavi
Firme digitali con crittografia a chiave asimmetrica​
Le firme digitali sono un'altra potente applicazione della crittografia a chiave asimmetrica. Forniscono autenticazione, integrità e non-repudiabilità abilitate dal fatto che nell'AKC le entità possiedono chiavi private uniche. L'idea di base sottostante i protocolli di firma è che i mittenti di messaggi sicuri firmeranno digitalmente i messaggi usando la propria chiave privata univoca. Il destinatario verificherà poi la firma digitale usando la chiave pubblica del mittente. Nell'AKC, le firme digitali possono essere implementate usando algoritmi specificamente progettati a tale scopo o usando crittosistemi generici.

Figura 4. Firme digitali con crittografia a chiave asimmetrica
Algoritmi dedicati alle firme digitali​
Attualmente, lo standard US Federal Information Processing Standard (FIPS) per le firme digitali è uno schema dedicato semplicemente intitolato Digital Signature Algorithm (DSA). In modo alquanto simile al protocollo Diffie-Hellman, il DSA utilizza le proprietà algebriche dell'esponenziale modulare e degli inversi moltiplicativi per la generazione e verifica delle firme.
L'algoritmo di firma digitale su curva ellittica (ECDSA) è una variante ECC del DSA, che fornisce le stesse funzionalità ma con chiavi significativamente più corte. Ciò comporta una maggiore efficienza, rendendolo una scelta popolare per sistemi con vincoli di risorse.
Sia DSA che ECDSA saranno illustrati più in dettaglio in seguito.
Schemi di firma digitale che utilizzano crittosistemi generici​
Oltre agli algoritmi dedicati, le firme digitali possono essere generate anche usando crittosistemi asimmetrici generici, come RSA.
RSA, che sarà discusso in dettaglio in una sezione successiva, sfrutta anch'esso gli inversi moltiplicativi modulari e l'esponenziazione modulare come operazioni fondamentali, ma le combina in una sequenza diversa rispetto al DSA. In RSA, il firmatario tipicamente crea un hash del messaggio e poi cifra l'hash con la propria chiave privata, creando la firma digitale. Qualsiasi parte può quindi verificare questa firma decifrandola con la chiave pubblica del firmatario e confrontandola con il messaggio sottoposto ad hashing.
Applicazioni della crittografia a chiave asimmetrica​
La crittografia a chiave asimmetrica è onnipresente nelle applicazioni tecnologiche digitali moderne. Le funzionalità di base dell'AKC descritte sopra costituiscono i mattoni di molti protocolli applicativi di livello superiore, tra cui:
-
Comunicazione Internet: La comunicazione sicura su Internet, come HTTPS, si basa fortemente sulla crittografia a chiave asimmetrica. Il Transport Layer Security (TLS) e il suo predecessore, Secure Sockets Layer (SSL), utilizzano la crittografia a chiave asimmetrica durante il processo di handshake iniziale per stabilire una chiave simmetrica, che viene poi utilizzata per il resto della sessione di comunicazione.
-
Autenticazione: La crittografia a chiave asimmetrica viene utilizzata per creare firme digitali, permettendo a un'entità di autenticare un documento o messaggio digitale come proveniente da un mittente specifico. Questo viene utilizzato in molti scenari, dalla verifica degli aggiornamenti software ai contratti digitali legalmente vincolanti.
-
Cifratura delle email: I protocolli di cifratura delle email come PGP (Pretty Good Privacy) e la sua alternativa open source GPG (GNU Privacy Guard) utilizzano la crittografia a chiave asimmetrica per garantire che solo il destinatario previsto possa leggere il contenuto dell'email.
-
Secure Shell (SSH): SSH è un protocollo per il login remoto sicuro e altri servizi di rete sicuri su una rete non sicura. Utilizza la crittografia a chiave asimmetrica per autenticare il server al client e, opzionalmente, il client al server.
-
VPN (rete privata virtuale): La cifratura a chiave asimmetrica viene utilizzata per stabilire connessioni sicure nelle VPN, garantendo comunicazioni sicure su reti pubbliche.
-
Blockchain e criptovalute: Le tecnologie blockchain, inclusi Bitcoin ed Ethereum, utilizzano la crittografia a chiave asimmetrica. Ad esempio, la proprietà di Bitcoin viene stabilita attraverso firme digitali che utilizzano la crittografia a chiave asimmetrica.
-
Autorità di certificazione: La crittografia a chiave asimmetrica viene utilizzata dalle autorità di certificazione (CA) per emettere e firmare certificati digitali, utilizzati nelle comunicazioni TLS, nella firma del codice, nella cifratura delle email e altro ancora. Un certificato digitale associa una chiave pubblica a un'entità specifica (ad esempio, una persona o un server).

Figura 5. Emissione e firma di certificati digitali con crittografia a chiave asimmetrica
Sicurezza della crittografia a chiave asimmetrica​
Diversi concetti crittografici si combinano per abilitare una crittografia a chiave asimmetrica sicura, tra cui:
Segretezza della chiave privata: Il requisito di sicurezza più fondamentale dell'AKC è che la chiave privata rimanga segreta. Tuttavia, poiché la chiave privata deve essere matematicamente collegata alla chiave pubblica, la segretezza della chiave privata richiede anche che sia computazionalmente impossibile derivare la chiave privata dalla conoscenza della chiave pubblica. Gli schemi di generazione delle chiavi nell'AKC si basano su problemi matematici computazionalmente difficili per facilitare questo requisito.
Funzionalità trapdoor: Nell'AKC, le operazioni di cifratura e decifratura coinvolgono chiavi complementari diverse di una singola coppia di chiavi. Il testo cifrato generato dalla cifratura usando una delle chiavi (ad esempio la chiave pubblica) deve essere incomprensibile a terze parti mentre deve essere facilmente decifrabile da chi detiene la chiave complementare (in questo caso la chiave privata). In altre parole, la cifratura dovrebbe assomigliare a una funzione unidirezionale con trapdoor tale che le terze parti non possano invertire l'operazione e recuperare il testo in chiaro, ma la chiave privata fornisce una trapdoor segreta che consente una facile inversione. I popolari algoritmi AKC utilizzano l'esponenziazione modulare per impostare il comportamento della funzione unidirezionale con trapdoor.
Casualità : Il processo di generazione delle chiavi dovrebbe anche sfruttare la casualità per garantire che le chiavi siano imprevedibili, poiché qualsiasi schema o prevedibilità nella generazione delle chiavi potrebbe essere sfruttata da un aggressore. La casualità viene utilizzata anche per il padding durante la cifratura per generare testi cifrati semanticamente sicuri e negli schemi di firma digitale per produrre firme uniche anche quando lo stesso messaggio viene firmato più volte. Per questo motivo, l'uso di generatori di numeri casuali robusti è una parte importante dell'AKC.
Dimensione grande delle chiavi: Come nel caso dell'SKC, chiavi di grandi dimensioni garantiscono protezione contro gli attacchi a forza bruta. Tuttavia, poiché chiavi di grandi dimensioni aumentano anche il costo computazionale del processo di cifratura e decifratura, una soluzione ottimale deve bilanciare considerazioni di sicurezza ed efficienza. La tabella seguente mostra le dimensioni tipiche delle chiavi per vari protocolli e applicazioni di crittografia a chiave asimmetrica:
| Protocollo | Dimensioni tipiche delle chiavi (in bit) | Applicazione |
|---|---|---|
| RSA | 1024 (deprecato), 2048, 3072, 4096 | Cifratura, firme digitali |
| DSA | 1024 (deprecato), 2048, 3072 | Firme digitali |
| DH | 2048, 3072, 4096 | Scambio di chiavi |
| ECDH | 224, 256, 384, 521 | Scambio di chiavi |
| ECDSA | 224, 256, 384, 521 | Firme digitali |
Infrastruttura a chiave pubblica: Nell'AKC, le chiavi private devono essere tenute segrete dai proprietari mentre le chiavi pubbliche vengono condivise. È necessario un meccanismo sicuro per gestire e distribuire queste chiavi pubbliche tra gli utenti. L'infrastruttura a chiave pubblica (PKI) fornisce un modo per farlo usando i certificati digitali. Un certificato digitale fornisce la prova dell'identità del proprietario della chiave pubblica ed è emesso da un'autorità fidata come un'autorità di certificazione (che fa parte di una PKI). Quindi, la PKI svolge un ruolo fondamentale nella sicurezza delle applicazioni moderne che utilizzano l'AKC, abilitando la gestione delle chiavi su larga scala (creando, gestendo, distribuendo e revocando certificati digitali).
Rischi di sicurezza per la crittografia a chiave asimmetrica​
Come indicato nella tabella precedente, i moderni algoritmi a chiave asimmetrica come RSA impiegano tipicamente dimensioni delle chiavi molto più grandi rispetto alle controparti a chiave simmetrica comunemente usate come AES-128. Anche i protocolli basati su ECC (ECDH ed ECDSA) che hanno chiavi più piccole impiegano un minimo di almeno 224 bit per le chiavi. Ciò implica a sua volta che un attacco a forza bruta che coinvolge una ricerca esaustiva dello spazio delle chiavi private per identificare la chiave corretta è computazionalmente intrattabile nel prossimo futuro. Questo rimane vero anche se i computer quantistici dovessero essere impiegati per questo compito. Pertanto, gli attacchi contro l'AKC di solito si concentrano sullo sfruttamento di altre potenziali debolezze di crittosistemi specifici. Alcune modalità di attacco ben documentate prendono di mira:
-
Debolezza algoritmica usando mezzi matematici e computazionali sofisticati per minare le ipotesi di durezza usate per formulare gli algoritmi a chiave asimmetrica. Ad esempio, la sicurezza di RSA è basata sulla difficoltà di fattorizzare grandi numeri primi, e recenti progressi computazionali hanno abilitato la fattorizzazione riuscita di chiavi RSA a 829 bit. Pertanto, RSA a 1024 bit è attualmente deprecato. Come verrà discusso in seguito, il rischio principale posto dai computer quantistici all'AKC rientra anch'esso in questa categoria.
-
Casualità imperfetta, che può portare a debolezze nel processo di generazione delle chiavi. Ad esempio, se il generatore di numeri casuali usato per creare le chiavi è difettoso e genera chiavi che non sono veramente casuali, un aggressore potrebbe essere in grado di prevedere le chiavi.
-
Difetti di implementazione come errori nell'implementazione degli algoritmi crittografici che rivelano inavvertitamente informazioni sulle chiavi. Ad esempio, un padding non corretto può potenzialmente rivelare dettagli sulle chiavi.
-
Canali laterali che si riferiscono alla perdita di informazioni dal sistema fisico che esegue la crittografia. Tali perdite potrebbero essere sotto forma di informazioni temporali, consumo di energia, o persino suoni, che possono essere sfruttati in un cosiddetto attacco a canale laterale. Ad esempio, analizzare quanto tempo impiegano le operazioni crittografiche potrebbe rivelare il numero di '1' in una chiave binaria. Questa perdita apparentemente innocente restringe significativamente lo spazio di ricerca, rivelando potenziali soluzioni a problemi che inizialmente sembravano insuperabili.
-
Scambio di chiavi intercettando le chiavi mentre vengono scambiate, come in un attacco man-in-the-middle (MITM). Il protocollo DH è suscettibile agli attacchi MITM se non vengono incorporati ulteriori passaggi di autenticazione.
-
Archiviazione delle chiavi mirando a rubare chiavi da archivi scarsamente sicuri. Ciò include attacchi fisici come la manipolazione o il furto di dispositivi di archiviazione.
Proteggere i crittosistemi a chiave asimmetrica dalla varietà di attacchi possibili è quindi un compito significativo che coinvolge considerazioni matematiche, hardware, software, logistiche e legali.
RSA​
L'algoritmo RSA (Rivest-Shamir-Adleman) è uno dei primi crittosistemi a chiave pubblica ed è ampiamente utilizzato per la trasmissione sicura dei dati. È un crittosistema versatile in quanto fornisce le operazioni necessarie per abilitare cifratura, decifratura, firme digitali e scambio di chiavi all'interno di un framework comune.
In questa sezione, illustreremo la crittografia a chiave asimmetrica (AKC) usando RSA attraverso semplici esempi.
Utilizzeremo lo scenario standard di due parti, Alice e Bob, che desiderano comunicare in modo sicuro usando l'AKC.
L'algoritmo RSA​
L'algoritmo RSA di base prevede quattro operazioni: generazione delle chiavi, distribuzione delle chiavi, cifratura e decifratura:
-
Generazione delle chiavi:
Le chiavi pubblica e privata vengono generate sulla base di principi matematici relativi ai numeri primi, dove calcolarle è facile, ma il contrario è difficile.
Faremo riferimento a queste:
- Chiave pubblica:
- Chiave privata:
Nota che è comune sia alla chiave pubblica che alla chiave privata, ed è noto come modulo. Dovremo usarlo in seguito.
Dettaglio matematico
- Scegli due numeri primi distinti, e .
- scelti casualmente (per sicurezza).
- Dovrebbero essere di grandezza simile, ma differire in lunghezza di qualche cifra, per rendere la fattorizzazione più difficile.
- I numeri primi possono essere scelti in modo efficiente usando un test di primalità .
- Calcola .
- è il modulo sia per le chiavi pubblica che privata.
- Calcola il totiente .
- Il totiente deve essere tenuto segreto e tipicamente viene eliminato dopo la generazione delle chiavi.
- Scegli un intero tale che e .
- ovvero, e devono essere coprimi.
- Questo numero forma l'esponente della chiave pubblica e viene tipicamente scelto come un numero piccolo per l'efficienza computazionale.
- Il numero primo viene spesso usato.
- Calcola per soddisfare la relazione di congruenza .
- Ovvero, è l'inverso moltiplicativo di modulo .
- Questo viene calcolato in modo più efficiente usando l'algoritmo euclideo esteso.
- Questo numero è l'esponente della chiave privata.
- La chiave pubblica è composta da , e la chiave privata è .
-
Distribuzione delle chiavi:
- La chiave pubblica viene resa pubblica a coloro che potrebbero desiderare di inviare un messaggio
- La chiave privata viene tenuta segreta.
-
Cifratura:
- Alice desidera inviare un messaggio a Bob. In questo caso un semplice intero
- Alice usa la chiave pubblica di Bob per cifrare il messaggio nel testo cifrato
Dettaglio matematico
- è un intero .
- , dove è il testo cifrato.
-
Decifratura:
- Bob riceve il testo cifrato
- Bob usa la sua chiave privata per decifrare il messaggio nel messaggio originale
Dettaglio matematico
- .
Questo è il profilo di base di RSA. In pratica, vengono applicati al testo in chiaro schemi di padding più sofisticati prima della cifratura per garantire che testi in chiaro uguali risultino in testi cifrati diversi. Questo previene una serie di possibili attacchi contro implementazioni ingenue di RSA e abilita la sicurezza semantica.
Illustrazione di RSA in Python​
Nelle seguenti celle di codice, illustriamo un semplice esempio dell'algoritmo RSA usando piccoli interi e poi dimostriamo applicazioni pratiche di distribuzione delle chiavi e firma digitale usando librerie Python che implementano RSA.
Nota: In questa sezione mostreremo i calcoli matematici in dettaglio come parte del codice Python
Generazione delle chiavi RSA​
Percorriamo passo per passo una semplice istanza dell'algoritmo RSA che impiega piccoli numeri primi.
Dovremo essere in grado di calcolare il massimo comune divisore di due interi, poiché sarà necessario per testare se due interi sono coprimi.
Spiegheremo un semplice modo per calcolarlo, ma è molto più efficiente con interi più grandi usare la funzione Python math.gcd.
# Added by doQumentation — required packages for this notebook
!pip install -q cryptography
import math
# Example function to compute the gcd (greatest common divisor)
def gcd(a, b):
if b == 0:
return a
else:
return gcd(b, a % b)
# let's calculate some examples using algorithm
n1 = gcd(50, 10)
n2 = gcd(99, 33)
n3 = gcd(59, 9)
# do the same with the python library call
m1 = math.gcd(50, 10)
m2 = math.gcd(99, 33)
m3 = math.gcd(59, 9)
# Confirm they are the same
assert n1 == m1
assert n2 == m2
assert n3 == m3
# They are - print out the values for explanation
print("gcd(50,10) =", m1)
print("gcd(99,33) =", m2)
print("gcd(59,9) =", m3)
La prima fase del flusso di lavoro RSA è la generazione delle chiavi. Viene avviata scegliendo due numeri primi, che devono essere tenuti segreti dall'entità che genera le chiavi.
# Choosing two prime numbers and keep them secret
p = 13
q = 19
print("The secret prime numbers p and q are:", p, q)
Successivamente, il modulo , che è semplicemente il prodotto dei due primi scelti, viene calcolato. Questo modulo verrà pubblicato come parte della chiave pubblica.
# Calculate n which is the modulus for both the public and private keys
n = p * q
print("modulus n (p*q)=", n)
La funzione totiente di Eulero viene calcolata successivamente, poiché è necessaria per l'operazione di inverso moltiplicativo modulare usata per determinare le chiavi in RSA. è anche tenuto segreto e tipicamente eliminato dopo la generazione delle chiavi.
# Compute Euler's totient function, φ(n) and keep it secret
phi = (p - 1) * (q - 1)
print("The secret Euler's function (totient) [phi(n)]:", phi)
Ora siamo pronti per calcolare le chiavi pubblica e privata. In RSA, ciascuna di esse è specificata da una tupla di due interi. La prima voce di ciascuna tupla è un intero distinto, e la seconda voce è il modulo comune a entrambe le chiavi.
La prima voce nella chiave pubblica può essere qualsiasi intero maggiore di 1 che sia coprimo con . Due interi sono coprimi se il loro massimo comune divisore è 1. Quindi usiamo la funzione math.gcd per trovare un intero coprimo con .
# Choose an integer e such that e and φ(n) are coprime
e = 2
while e < phi:
if math.gcd(e, phi) == 1:
break
else:
e += 1
print("Public Key (e):", e)
La chiave privata richiede un intero , che è l'inverso moltiplicativo di modulo ; ovvero, soddisfa la relazione di congruenza . Per questa semplice illustrazione dove trattiamo piccoli interi, possiamo semplicemente iterare sugli interi positivi per trovare un adatto. In contesti realistici, viene usato a tale scopo l'efficiente algoritmo euclideo esteso.
# Compute a value for d such that (d * e) % φ(n) = 1
d = 1
while True:
if (d * e) % phi == 1:
break
else:
d += 1
print("Private Key (d):", d)
Formiamo ora le tuple , che costituiscono rispettivamente le chiavi pubblica e privata. La chiave pubblica viene quindi pubblicata, mentre la chiave privata viene tenuta segreta.
# Public and Private Key pair
public = (e, n)
private = (d, n)
print(f"The Public key is {public} and Private Key is {private}")
La cifratura e la decifratura in RSA utilizzano l'operazione di esponenziazione modulare. Inoltre, le chiavi pubblica e privata sono complementari nel senso che entrambe possono essere usate per cifrare un messaggio che l'altra può poi decifrare.
Qui illustriamo il caso in cui la chiave pubblica viene usata per la cifratura e la chiave privata per la decifratura, definendo una funzione Python per ciascuna operazione.
Cifriamo e decifriamo quindi un messaggio intero .
# Encryption function
def encrypt(plain_text):
return (plain_text**e) % n
# Decryption function
def decrypt(cipher_text):
return (cipher_text**d) % n
# Simple message to encode
msg = 9
# encrypt then decrypt
enc_msg = encrypt(msg)
dec_msg = decrypt(enc_msg)
print("Original Message:", msg)
print("Encrypted Message:", enc_msg)
print("Decrypted Message:", dec_msg)
Mentre i piccoli interi usati sopra sono utili per delineare facilmente le idee principali dell'algoritmo RSA, nelle applicazioni reali RSA richiede l'uso di interi molto grandi. Ad esempio, RSA a 2048 bit prevede l'uso di un modulo lungo 2048 bit, i cui equivalenti interi decimali sono intorno a 10. Questi numeri davvero enormi sono necessari per la sicurezza pratica di RSA.
Scambio di chiavi simmetriche con RSA​
Come discusso in precedenza, l'AKC consente a due parti che desiderano comunicare di stabilire in modo sicuro un segreto condiviso, che può essere usato, ad esempio, come chiave segreta per la successiva cifratura simmetrica del testo in chiaro in blocco.
Consideriamo il seguente scenario. Alice e Bob vogliono usare l'SKC per cifrare e decifrare i messaggi. Tuttavia, prima che questo processo possa essere avviato, devono concordare una chiave segreta comune. Un'opzione è che una delle parti — diciamo Alice — generi una chiave segreta e poi la trasmetta in modo sicuro a Bob. Per ottenere questo trasferimento sicuro, Alice decide di usare RSA come meccanismo di incapsulamento delle chiavi (KEM).
Questo comporta i seguenti passaggi:
- Prima, Alice genera una chiave simmetrica casuale, che intende condividere con Bob.
- Poi, Bob genera una coppia di chiavi asimmetriche e rende disponibile la sua chiave pubblica su un canale appropriato.
- Successivamente, Alice usa la chiave pubblica di Bob per cifrare la chiave simmetrica, incapsulandola così in un testo cifrato.
- Poi, Alice trasmette il testo cifrato su un canale affidabile ma non necessariamente sicuro.
- Infine, Bob riceve il testo cifrato e lo decifra usando la sua chiave privata. Ora ha accesso alla chiave simmetrica generata da Alice.

Figura 1. Scambio di chiavi simmetriche con RSA
Esempio di scambio di chiavi basato su padding in Python​
Un flusso di lavoro pratico che utilizza RSA per lo scambio di chiavi non interattivo basato su padding viene ora illustrato usando la libreria Python cryptography.
Importa i moduli necessari dalla libreria Python cryptography. Se necessario, questa libreria può essere installata usando il comando pip install cryptography.
Alice genera quindi una chiave segreta casuale, che intende trasmettere a Bob.
# pip install cryptography
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
symmetric_key = Fernet.generate_key()
print(f"\nSymmetric key generated by Alice: {symmetric_key}")
Usando il modulo rsa dalla libreria cryptography, Bob genera una coppia di chiavi e poi trasmette la sua chiave pubblica. Chiunque può intercettare la chiave pubblica e leggere i numeri pubblici che formano la chiave.
# Bob generates a 2048-bit RSA key pair
bob_private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
bob_public_key = bob_private_key.public_key()
print(f"Public key broadcast by Bob: {bob_public_key}")
print(f"\nPublic numbers in Bobs' public key: {bob_public_key.public_numbers()}")
A questo punto, assumiamo che Alice abbia ricevuto la chiave pubblica trasmessa da Bob. La symmetric_key di Alice può ora essere cifrata usando la chiave pubblica di Bob per produrre il testo cifrato. In un contesto realistico, Alice userà anche metodi di padding aggiuntivi come OAEP per garantire la sicurezza semantica per le sue comunicazioni con Bob.
# Encryption
ciphertext = bob_public_key.encrypt(
symmetric_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
print("Ciphertext:", ciphertext)
Alice trasmette quindi il testo cifrato su un canale aperto, fiduciosa che solo Bob con la corrispondente chiave privata sarà in grado di decifrarlo. Assumiamo che Bob abbia ricevuto il testo cifrato e possa ora decifrarlo usando la sua chiave privata riservata.
# Bob decrypts ciphertext to access the symmetric key
decrypted_symmetric_key = bob_private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
print("Decrypted key:", decrypted_symmetric_key)
assert decrypted_symmetric_key == symmetric_key
A questo punto, sia Alice che Bob hanno accesso alla chiave simmetrica segreta, che possono usare per applicazioni SKC.
Simulazione di un meccanismo di incapsulamento delle chiavi con RSA in Python​
Nel seguente flusso di lavoro, illustriamo l'uso di RSA per simulare un meccanismo di incapsulamento delle chiavi (KEM) mediante il quale un messaggio segreto casuale sufficientemente lungo viene scambiato in modo sicuro e successivamente convertito in un segreto condiviso della lunghezza appropriata usando una KDF.
Anche in questo caso Alice e Bob vogliono stabilire un segreto condiviso in modo non interattivo e Alice è la parte che decide quale segreto usare.
Iniziamo importando alcune librerie Python necessarie.
Bob genera quindi la sua coppia di chiavi RSA e trasmette la sua chiave pubblica.
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
from os import urandom
# Bob's RSA key pair
private_key_Bob = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key_Bob = private_key_Bob.public_key()
print("Bob's private and public keys created")
Alice genera prima un lungo segreto casuale da cui verrà eventualmente derivato il segreto condiviso. In un KEM puro, il lungo segreto sarà un elemento casuale della struttura algebrica sottostante il crittosistema. Nel caso di RSA a 2048 bit, questo sarebbe un intero casuale modulo il modulo RSA a 2048 bit. Poiché un KEM puro non richiede padding aggiuntivo, ma in questo esempio stiamo solo simulando un KEM usando RSA e la libreria cryptography richiede l'uso del padding quando si cifra con RSA, utilizzeremo un lungo segreto leggermente più corto che tuttavia è molto più lungo di una chiave AES standard a 256 bit.
Alice_long_secret = urandom(160) # A 160 byte or 1280 bit random message
print("Alice's secret created")
Successivamente Alice cifra il lungo segreto usando la chiave pubblica di Bob e il segreto cifrato viene comunicato a Bob.
Alice_encrypted_secret = public_key_Bob.encrypt(
Alice_long_secret,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
print("Alice's secret encrypted")
Bob decifra il segreto cifrato ricevuto da Alice usando la sua chiave privata.
Bob_decrypted_secret = private_key_Bob.decrypt(
Alice_encrypted_secret,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
assert Alice_long_secret == Bob_decrypted_secret, "Secrets do not match!"
# if we get here they match
print("Secrets match")
Infine sia Alice che Bob applicano separatamente una funzione di derivazione delle chiavi (KDF) concordata sul lungo segreto per derivare la chiave simmetrica.
Nota che il processo prevede un protocollo di hashing e l'uso di un salt casuale che garantisce unicità e imprevedibilità della chiave simmetrica condivisa nel caso in cui il lungo segreto venga mai riutilizzato (non raccomandato). Tuttavia il salt stesso non deve essere segreto e una volta generato casualmente, diciamo da Alice in questo esempio, può essere trasmesso a Bob insieme al lungo segreto cifrato.
Assumeremo quindi che sia Alice che Bob abbiano accesso allo stesso salt casuale.
def key_derivation_function(secret, salt):
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32, # Desired key length
salt=salt,
info=None,
backend=None,
)
return hkdf.derive(secret)
common_salt = urandom(16) # Random salt accessible to both Alice and Bob
symmetric_key_Alice = key_derivation_function(Alice_long_secret, common_salt)
symmetric_key_Bob = key_derivation_function(Bob_decrypted_secret, common_salt)
assert symmetric_key_Alice == symmetric_key_Bob, "Derived keys do not match!"
print(
f"A symmetric key of length {len(symmetric_key_Alice)*8} bits was successfully derived by both Alice and Bob!"
)
Firme digitali con RSA​
Estenderemo ora lo scenario di comunicazione riservata sopra con Alice e Bob a uno che include anche la validazione con l'aiuto di una firma digitale.
Come prima, Alice invierà riservatamente a Bob un messaggio che incapsula una chiave simmetrica, ma lo firmerà digitalmente in modo che Bob, alla ricezione del messaggio, possa verificare che sia stata Alice ad originarlo e che il contenuto del messaggio non sia stato manomesso durante la trasmissione.
Più in generale, è auspicabile abilitare la validazione senza compromettere la riservatezza, dove qualsiasi parte interessata è in grado di verificare l'integrità , l'autenticità e stabilire la non-repudiabilità rispetto a una data comunicazione, anche se quella parte non ha accesso al messaggio in chiaro effettivo.
Considereremo questo contesto generale che comporta quindi i seguenti passaggi:
- Prima, sia Bob che Alice rendono disponibili le loro chiavi pubbliche su un canale aperto.
- Poi, Alice cifra il testo in chiaro usando la chiave pubblica di Bob, creando un testo cifrato.
- Successivamente, Alice crea un hash del testo cifrato con una funzione hash e cifra ulteriormente il testo cifrato sottoposto ad hashing usando la sua chiave privata. Questo hash cifrato è la firma.
- Poi, Alice trasmette sia il testo cifrato che la firma su un canale aperto.
- Poi, Bob usa la chiave pubblica di Alice per decifrare la firma, rivelando il testo cifrato sottoposto ad hashing.
- Successivamente, poiché Bob ha anche accesso al testo cifrato stesso, utilizza la stessa funzione hash usata da Alice per ricreare una seconda istanza del testo cifrato sottoposto ad hashing. Se quest'ultimo corrisponde a quello ottenuto decifrandando la firma di Alice, allora il messaggio è validato, anche se il testo cifrato stesso non è ancora stato decifrato.
- Infine, Bob, avendo validato il messaggio, decifra il testo cifrato usando la propria chiave privata.

Figura 2. Firme digitali con RSA
Questo flusso di lavoro per una firma digitale è illustrato di seguito.
Importiamo di nuovo alcuni moduli utili dalla libreria cryptography.
Come prima, Alice intende inviare in modo sicuro una chiave simmetrica a Bob, ma desidera anche firmarla digitalmente. In questo caso, abbiamo bisogno delle chiavi pubbliche sia di Alice che di Bob. Pertanto, il primo passo è che sia Alice che Bob creino la propria coppia di chiavi usando RSA e trasmettano la propria chiave pubblica al mondo.
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
# Generate keys for Bob
bob_private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
bob_public_key = bob_private_key.public_key()
# Generate keys for Alice
alice_private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
alice_public_key = alice_private_key.public_key()
print("Private and Public keys generated for Bob and Alice.")
Nel passo successivo, come prima, Alice usa la chiave pubblica di Bob per cifrare la chiave simmetrica e prepara il testo cifrato.
# Alice encrypts the message using Bob's public key
ciphertext = bob_public_key.encrypt(
symmetric_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
print("ciphertext of symmetric key: ", ciphertext)
In questa fase, invece di trasmettere semplicemente il testo cifrato, Alice intende allegarvi una firma digitale in modo da poter dimostrare a Bob di essere la mittente del messaggio. Questo viene fatto in due passaggi:
- Crea un hash del testo cifrato usando un algoritmo di hashing.
- Cifra l'hash usando la chiave privata di Alice, il che equivale a una firma.
# Alice signs the ciphertext using her private key
digest = hashes.Hash(hashes.SHA256())
digest.update(ciphertext)
hash_to_sign = digest.finalize()
signature = alice_private_key.sign(
hash_to_sign,
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
Prehashed(hashes.SHA256()),
)
print("signature: ", signature)
Alice trasmette quindi il testo cifrato e la firma su una rete in modo che Bob possa intercettarli entrambi.
# Bob receives the ciphertext and signature
received_ciphertext = ciphertext
received_signature = signature
# Send signature and ciphertext here
print("Sending ciphertext and signature.....")
Dal lato di Bob, il primo compito è verificare l'integrità e l'autenticità del testo cifrato. Per farlo, Bob crea un hash del testo cifrato ricevuto usando lo stesso algoritmo di hashing usato da Alice.
# Bob creates a hash of the ciphertext using the same algorithm used by Alice
digest = hashes.Hash(hashes.SHA256())
digest.update(received_ciphertext)
hash_to_verify = digest.finalize()
print("hash to verify: ", hash_to_verify)
Poi, Bob decifra la firma ricevuta usando la chiave pubblica di Alice. Poiché Alice ha usato la sua chiave privata per creare la firma, Bob è in grado di decifrarla usando la chiave pubblica di Alice. La firma decifrata non è altro che un hash del testo cifrato creato dal lato di Alice. Se l'hash creato da Bob corrisponde alla firma decifrata, allora Bob ha verificato che il testo cifrato che ha ricevuto non è stato manomesso e che è stata Alice a firmare il testo cifrato.
Nel codice Python seguente, queste operazioni sono combinate in una utile funzione di utilità chiamata verify fornita da un oggetto associato alla chiave pubblica di Alice.
from cryptography.exceptions import InvalidSignature
def is_signature_valid(public_key, signature, data_hash):
try:
public_key.verify(
signature,
data_hash,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
),
Prehashed(hashes.SHA256()),
)
return True
except InvalidSignature:
return False
if is_signature_valid(alice_public_key, received_signature, hash_to_verify):
print("The signature is valid.")
else:
print("The signature is not valid.")
Avendo verificato l'integrità e l'autenticità del testo cifrato ricevuto, Bob può quindi decifrarlo usando la sua chiave privata poiché Alice ha creato il testo cifrato usando la chiave pubblica di Bob.
# Bob decrypts the message using his private key
decrypted_message = bob_private_key.decrypt(
received_ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
print("Decrypted message:", decrypted_message.decode())
Nello scenario di firma digitale sopra descritto, qualsiasi parte — non solo Bob — può verificare che Alice sia la mittente del testo cifrato perché tutti possono accedere alla chiave pubblica di Alice, al testo cifrato e alla firma digitale. Inoltre, dopo aver inviato il testo cifrato e la firma, Alice non può in seguito negare di averlo fatto perché la firma può essere decifrata in un hash significativo usando solo la sua chiave pubblica. Questo stabilisce la non-repudiabilità .
Abilitando la distribuzione sicura delle chiavi e supportando la non-repudiabilità , la crittografia a chiave pubblica pone le basi solide per una comunicazione digitale sicura.
Violare RSA​
L'utilità e la sicurezza dell'algoritmo RSA descritto sopra si fondano su due assunzioni matematiche:
-
Trovare l'inverso moltiplicativo modulare avendo accesso solo a è computazionalmente infattibile.
-
Nell'ambito RSA, l'operazione di esponenziazione modulare si comporta come una funzione trapdoor unidirezionale. Quando viene usata per la cifratura, produce un testo cifrato incomprensibile e, senza accesso alla chiave privata, invertire l'operazione per recuperare il testo in chiaro dal testo cifrato non è fattibile. Tuttavia, con accesso alla chiave privata, che funge da trapdoor, il testo cifrato può essere facilmente decifrato.
L'attacco più noto all'algoritmo RSA mira a minare l'assunzione 1 recuperando efficientemente il numero di chiave privata tramite la fattorizzazione del modulo . Come illustrato di seguito, è facile recuperare se si ha accesso ai fattori primi e di o al totiente . Ricorda che , e vengono tenuti segreti durante la generazione delle chiavi e scartati dopo. Una terza parte che recupera queste informazioni usando un computer classico o quantistico scopre essenzialmente la chiave privata, violando RSA. Quindi, la fattorizzazione in numeri primi è la primitiva computazionale fondamentale necessaria per violare RSA.
Il calcolo classico e RSA​
La fattorizzazione in numeri primi di interi di grandi dimensioni è nota per presentare una scalabilità super-polinomiale o subesponenziale sui computer classici. Il miglior algoritmo classico conosciuto per la fattorizzazione di interi maggiori di è il crivello del campo numerico generale (GNFS)
Dettaglio matematico
in funzione dell'intero da fattorizzare.
Questa scalabilità è super-polinomiale nel numero di bit necessari per rappresentare .
Pertanto, la fattorizzazione in numeri primi è considerata inefficiente sui computer classici.
Attualmente, i più grandi interi fattorizzati su hardware classico sono nell'ordine di 829 bit o 250 cifre decimali. Dato il crescita esponenziale della potenza di calcolo classica testimoniata negli ultimi decenni, RSA a 1024 bit non è più considerato sicuro nel breve termine ed è ora deprecato. Tuttavia, nel prevedibile futuro, la fattorizzazione di interi a 2048 bit la cui grandezza è nell'ordine di è attualmente considerata infattibile sui sistemi classici, suggerendo una duratura vitalità . L'avvento dei computer quantistici, tuttavia, vanifica questa valutazione.
L'algoritmo quantistico di Shor e RSA​
Probabilmente l'algoritmo quantistico più conosciuto oggi è l'algoritmo di Shor per trovare i fattori primi degli interi. Quando introdotto da Peter Shor nel 1994, fu riconosciuto come il primo algoritmo quantistico che offriva un speedup super-polinomiale rispetto agli algoritmi classici su un problema di grande importanza pratica, ovvero la fattorizzazione in numeri primi.
L'algoritmo di Shor può fattorizzare i numeri primi con dove è il numero di bit.
Spiegazione matematica dell'algoritmo di Shor
Nel contesto di RSA, l'algoritmo di Shor funziona sfruttando la periodicità della funzione esponenziale modulare e fornisce una primitiva quantistica per trovare il periodo che consente una fattorizzazione efficiente del modulo .
Di seguito una sintesi semplificata dello schema generale di Shor per violare RSA:
-
Dato il modulo , pubblicato come parte della chiave pubblica, scegli un numero coprimo con , cioè . Poiché sappiamo che ha esattamente due fattori primi , quasi qualsiasi numero minore di scelto a caso è probabilmente coprimo con .
-
Avendo scelto , trova l'esponente tale che . Questo implica . L'esistenza di un esponente tale che vale la suddetta congruenza è garantita dalla proprietà di periodicità dell'esponenziazione modulare.
-
Se è pari,