Uno sguardo pratico alle idee di Daniel J. Bernstein sulla security-by-construction—da qmail a Curve25519—e cosa significa nella pratica una crittografia “semplice e verificabile”.

Security-by-construction significa costruire un sistema in modo che gli errori comuni siano difficili da commettere e che il danno degli errori inevitabili sia limitato. Invece di affidarsi a una lunga checklist ("ricordati di validare X, sanificare Y, configurare Z…"), progetti il software in modo che la strada più sicura sia anche la più semplice.
Pensa a una confezione a prova di bambino: non presume che tutti saranno perfettamente attenti; presume che gli esseri umani siano stanchi, occupati e a volte sbaglino. Un buon design riduce la quantità di "comportamento perfetto" richiesta a sviluppatori, operatori e utenti.
I problemi di sicurezza spesso si annidano nella complessità: troppe funzionalità, troppe opzioni, troppe interazioni tra componenti. Ogni manopola in più può creare una nuova modalità di guasto—un modo inaspettato in cui il sistema può rompersi o essere usato male.
La semplicità aiuta in due modi pratici:
Non si tratta di minimalismo fine a sé stesso. Si tratta di mantenere l’insieme dei comportamenti sufficientemente piccolo da poterlo davvero capire, testare e ragionare su cosa succede quando qualcosa va storto.
Questo articolo usa il lavoro di Daniel J. Bernstein come insieme di esempi concreti di security-by-construction: come qmail mirava a ridurre i modi di guasto, come il pensiero constant-time evita fughe invisibili, e come Curve25519/X25519 e NaCl spingono verso una crittografia difficile da usare male.
Cosa non farà: fornire una storia completa della crittografia, dimostrare la sicurezza di algoritmi, o dichiarare che esiste una singola “migliore” libreria per ogni prodotto. E non pretenderà che i buoni primitivi risolvano tutto—i sistemi reali falliscono ancora per la gestione delle chiavi, errori d’integrazione e gap operativi.
L’obiettivo è semplice: mostrare pattern di progettazione che rendono gli esiti sicuri più probabili, anche quando non sei uno specialista di crittografia.
Daniel J. Bernstein (spesso “DJB”) è un matematico e informatico il cui lavoro ricorre spesso nell’ingegneria della sicurezza pratica: sistemi di posta (qmail), primitivi e protocolli crittografici (in particolare Curve25519/X25519) e librerie che confezionano la crittografia per l’uso nel mondo reale (NaCl).
La gente cita DJB non perché abbia scritto l’unico modo “giusto” per fare sicurezza, ma perché i suoi progetti condividono un insieme coerente di istinti ingegneristici che riducono il numero di modi in cui le cose possono andare male.
Un tema ricorrente è interfacce più piccole e più strette. Se un sistema espone meno punti d’ingresso e meno scelte di configurazione, è più facile da revisionare, più facile da testare e più difficile da usare male per sbaglio.
Un altro tema è assunzioni esplicite. I fallimenti di sicurezza spesso nascono da aspettative non dette—sulla casualità, sul comportamento temporale, sulla gestione degli errori o su come le chiavi sono conservate. Gli scritti e le implementazioni di DJB tendono a rendere concreto il modello di minaccia: cosa è protetto, da chi e in quali condizioni.
Infine, c’è un’inclinazione verso default più sicuri e correttezza noiosa. Molti design in questa tradizione cercano di eliminare gli spigoli taglienti che portano a bug sottili: parametri ambigui, modalità opzionali e scorciatoie prestazionali che perdono informazioni.
Questo articolo non è una biografia o un dibattito sulle personalità. È una lettura ingegneristica: quali pattern si osservano in qmail, nel pensiero constant-time, in Curve25519/X25519 e in NaCl, e come quei pattern si traducono nel costruire sistemi più semplici da verificare e meno fragili in produzione.
qmail è stato creato per risolvere un problema poco glamour: consegnare posta in modo affidabile trattando il server di posta come un bersaglio ad alto valore. I sistemi di posta stanno su Internet, accettano input ostile tutto il giorno e toccano dati sensibili (messaggi, credenziali, regole di instradamento). Storicamente, un bug in un demonio di posta monolitico poteva significare una compromissione completa del sistema—o la perdita silenziosa di messaggi che nessuno nota finché non è troppo tardi.
Un’idea definitoria in qmail è spezzare la “consegna della posta” in piccoli programmi che fanno una sola cosa: ricezione, coda, consegna locale, consegna remota, ecc. Ogni pezzo ha un’interfaccia stretta e responsabilità limitate.
Quella separazione conta perché i guasti diventano locali:
Questa è security-by-construction in forma pratica: progetti il sistema in modo che “un errore” sia meno probabile che diventi “fallimento totale”.
qmail mostra anche abitudini che si traducono bene oltre la posta:
La conclusione non è “usa qmail”. È che spesso si ottengono grandi guadagni di sicurezza riprogettando intorno a meno modalità di guasto—prima ancora di scrivere più codice o aggiungere più manopole.
“Superficie d’attacco” è la somma di tutti i posti dove il tuo sistema può essere toccato, spinto o ingannato a fare la cosa sbagliata. Un’analogia utile è una casa: ogni porta, finestra, apertura del garage, chiave di riserva e sportello per le consegne è un potenziale punto d’accesso. Puoi installare serrature migliori, ma sei anche più al sicuro avendo meno punti d’accesso.
Il software è lo stesso. Ogni porta che apri, formato di file che accetti, endpoint di amministrazione che esponi, manopola di configurazione che aggiungi e hook per plugin che supporti aumenta il numero di modi in cui le cose possono fallire.
Un’“interfaccia stretta” è un’API che fa meno, accetta meno variazioni e rifiuta input ambigui. Questo spesso sembra restrittivo, ma è più facile da mettere in sicurezza perché ci sono meno percorsi di codice da auditare e meno interazioni sorprendenti.
Considera due design:
Il secondo design riduce ciò che gli attaccanti possono manipolare. Riduce anche ciò che il tuo team può configurare male per errore.
Le opzioni moltiplicano i test. Se supporti 10 interruttori, non hai 10 comportamenti—hai combinazioni. Molti bug di sicurezza vivono in quelle giunzioni: “questa flag disabilita un controllo”, “questa modalità salta la validazione”, “questa impostazione legacy aggira i rate limit”. Le interfacce strette trasformano la “sicurezza scegli-la-tu” in un unico percorso ben illuminato.
Usa questo per individuare la superficie d’attacco che cresce silenziosamente:
Quando non puoi ridurre l’interfaccia, rendila rigorosa: valida presto, rifiuta campi sconosciuti e tieni le “feature power” dietro endpoint separati e ben scanditi.
Il comportamento “constant-time” significa che un calcolo richiede (più o meno) lo stesso tempo indipendentemente da valori segreti come chiavi private, nonce o bit intermedi. L’obiettivo non è essere veloci; è essere noiosi: se un attaccante non può correlare il tempo di esecuzione ai segreti, è molto più difficile estrarre quei segreti osservando.
Le fughe di timing contano perché gli attaccanti non devono sempre violare la matematica. Se possono eseguire la stessa operazione molte volte (o osservarla su hardware condiviso), differenze piccole—microsecondi, nanosecondi, persino effetti di cache—possono rivelare pattern che si accumulano fino al recupero della chiave.
Anche codice “normale” può comportarsi diversamente a seconda dei dati:
if (secret_bit) { ... } cambia il flusso di controllo e spesso il tempo di esecuzione.Non serve leggere l’assembly per ottenere valore da un audit:
if dipendenti da segreti, indici di array, loop con terminazione basata su segreti e logiche “fast path/slow path”.Il pensiero constant-time è meno eroico e più disciplinare: progetta codice in modo che i segreti non possano pilotare il timing.
Lo scambio di chiavi su curve ellittiche è un modo per cui due dispositivi creano lo stesso segreto condiviso pur inviando solo messaggi “pubblici” sulla rete. Ciascuna parte genera un valore privato (segreto) e un valore pubblico corrispondente (sicuro da inviare). Dopo lo scambio dei valori pubblici, entrambe combinano il proprio valore privato con il pubblico dell’altra per ottenere lo stesso segreto condiviso. Un eavesdropper vede i valori pubblici ma non può ricostruire il segreto condiviso in modo praticabile, quindi le due parti possono derivare chiavi di cifratura e comunicare privatamente.
Curve25519 è la curva sottostante; X25519 è la funzione standardizzata che indica “fai questa cosa specifica” per lo scambio di chiavi costruita sopra di essa. Il loro appeal è in gran parte security-by-construction: meno possibilità di commettere errori, meno scelte di parametri e meno modi per selezionare un’impostazione non sicura.
Sono anche veloci su una vasta gamma di hardware, cosa che conta per server con molte connessioni e per telefoni che vogliono risparmiare batteria. Il design incoraggia implementazioni più facili da mantenere constant-time (aiutando a resistere agli attacchi di timing), il che riduce il rischio che un attaccante abile possa estrarre segreti misurando piccole differenze prestazionali.
X25519 ti dà l’accordo di chiave: aiuta due parti a derivare un segreto condiviso per la cifratura simmetrica.
Non fornisce autenticazione da solo. Se esegui X25519 senza verificare anche con chi stai parlando (per esempio con certificati, firme o una chiave pre-condivisa), puoi comunque essere ingannato a parlare in modo sicuro con la parte sbagliata. In altre parole: X25519 aiuta a prevenire l’intercettazione, ma non impedisce l’usurpazione da solo.
NaCl (Networking and Cryptography library) è stato costruito con un obiettivo semplice: rendere difficile agli sviluppatori applicativi montare per errore una crittografia non sicura. Invece di offrire un buffet di algoritmi, modalità, regole di padding e manopole di configurazione, NaCl ti spinge verso un piccolo insieme di operazioni di alto livello già cablate in modo sicuro.
Le API di NaCl hanno nomi in base a cosa vuoi fare, non a quali primitivi vuoi assemblare.
crypto_box (“box”): cifratura autenticata a chiave pubblica. Gli dai la tua chiave privata, la chiave pubblica del destinatario, un nonce e un messaggio. Ottieni un ciphertext che (a) nasconde il messaggio e (b) dimostra che proviene da qualcuno che conosce la chiave giusta.crypto_secretbox (“secretbox”): cifratura autenticata a chiave condivisa. Stesso concetto, ma con una singola chiave segreta condivisa.Il vantaggio principale è che non scegli separatamente “modalità di cifratura” e “algoritmo MAC” e poi speri di averli combinati correttamente. I default di NaCl impongono composizioni moderne e resistenti all’uso scorretto (encrypt-then-authenticate), quindi i modi di fallimento comuni—come dimenticare i controlli di integrità—sono molto meno probabili.
La rigidità di NaCl può sembrare limitante se hai bisogno di compatibilità con protocolli legacy, formati specializzati o algoritmi richiesti da norme. Scambi “posso regolare ogni parametro” con “posso distribuire qualcosa di sicuro senza diventare un esperto di crittografia”.
Per molti prodotti, questo è proprio il punto: vincola lo spazio di progettazione così che esistano meno bug. Se hai davvero bisogno di personalizzazione, puoi scendere ai primitivi di basso livello—ma stai tornando volontariamente sugli spigoli taglienti.
“Secure by default” significa che l’opzione più sicura e ragionevole è quella che ottieni quando non fai nulla. Se uno sviluppatore installa una libreria, copia un esempio rapido o usa i default del framework, il risultato dovrebbe essere difficile da usare male e difficile da indebolire per errore.
I default contano perché la maggior parte dei sistemi reali gira con essi. I team lavorano veloci, la documentazione viene letta di sfuggita e la configurazione cresce organicamente. Se il default è “flessibile”, spesso si traduce in “facile da configurare male”.
I fallimenti crittografici non sono sempre causati da “math sbagliata”. Spesso derivano dalla scelta di un’impostazione pericolosa perché era disponibile, familiare o comoda.
Trappole comuni nei default includono:
Preferisci stack che rendono la strada sicura quella più semplice: primitivi rivisti, parametri conservativi e API che non ti chiedono di prendere decisioni fragili. Se una libreria ti costringe a scegliere tra dieci algoritmi, cinque modalità e molte codifiche, ti sta chiedendo di fare security engineering via configurazione.
Quando puoi, scegli librerie e design che:
Security-by-construction è, in parte, rifiutare di trasformare ogni decisione in un dropdown.
“Verificabile” per la maggior parte dei team di prodotto non significa “formalmente provato”. Significa poter costruire fiducia rapidamente, ripetutamente e con meno opportunità di fraintendere cosa fa il codice.
Un codebase diventa più verificabile quando:
Ogni ramo, modalità e feature opzionale moltiplica ciò di cui i reviewer devono occuparsi. Interfacce più semplici restringono lo spazio degli stati possibili, migliorando la qualità della review in due modi:
Tieni le cose noiose e ripetibili:
Questa combinazione non sostituirà la revisione esperta, ma alza il livello minimo: meno sorprese, rilevamento più rapido e codice che puoi davvero ragionare.
Anche se scegli primitive ben considerate come X25519 o un’API minimale in stile NaCl, i sistemi si rompono ancora nelle parti sporche: integrazione, codifica e operazioni. La maggior parte degli incidenti reali non è “la matematica era sbagliata”, ma “la matematica è stata usata male”.
Gli errori di gestione delle chiavi sono comuni: riusare chiavi a lungo termine dove serve una chiave effimera, conservare chiavi nel controllo versione, o confondere "chiave pubblica" e "chiave segreta" perché sono entrambe semplici array di byte.
L’uso scorretto dei nonce è un recidivo. Molte schede di cifratura autenticata richiedono un nonce unico per chiave. Duplicare un nonce (spesso tramite reset di contatori, race multi-processo o assunzioni “abbastanza casuale”) può compromettere la confidenzialità o l’integrità.
Problemi di codifica e parsing creano fallimenti silenziosi: confusione base64 vs hex, perdita di zeri iniziali, endianness incoerente o accettare più codifiche che poi confrontano in modo diverso. Questi bug possono trasformare una “firma verificata” in una “qualcosa verificato” sbagliata.
La gestione degli errori può essere pericolosa in entrambe le direzioni: restituire errori dettagliati che aiutano un attaccante, o ignorare i fallimenti di verifica e proseguire comunque.
I segreti trapelano attraverso log, report di crash, analytics ed endpoint di debug. Le chiavi finiscono anche in backup, immagini di VM e variabili d’ambiente condivise troppo ampiamente. Nel frattempo, aggiornamenti di dipendenze (o la loro assenza) possono lasciarti bloccato su un’implementazione vulnerabile anche se il design era solido.
I buoni primitivi non producono automaticamente un prodotto sicuro. Più scelte esponi—modalità, padding, codifiche, "tweak" custom—più modi i team possono accidentalmente costruire qualcosa di fragile. Un approccio security-by-construction parte scegliendo un percorso ingegneristico che riduca i punti di decisione.
Usa una libreria di alto livello (API one-shot come “cifra questo messaggio per quel destinatario”) quando:
Componi primitivi di basso livello (AEAD, hash, scambio di chiavi) solo quando:
Una regola utile: se il tuo design doc contiene “sceglieremo la modalità più tardi” o “saremo semplicemente attenti con i nonce”, stai già pagando per troppe manopole.
Chiedi risposte concrete, non linguaggio di marketing:
Tratta la crittografia come codice critico per la sicurezza: mantieni l’API piccola, pinna versioni, aggiungi known-answer tests e fai fuzzing su parsing/serializzazione. Documenta cosa non supporterai (algoritmi, formati legacy) e costruisci migrazioni invece di “switch di compatibilità” che rimangono per sempre.
Security-by-construction non è uno strumento nuovo da comprare—è un insieme di abitudini che rendono più difficile creare intere categorie di bug. Il filo comune nell’ingegneria in stile DJB è: mantieni le cose abbastanza semplici da poterle comprendere, rendi le interfacce abbastanza strette da limitare l’uso scorretto, scrivi codice che si comporti allo stesso modo anche sotto attacco e scegli default che falliscano in modo sicuro.
Se vuoi una checklist strutturata per questi passaggi, considera di aggiungere una pagina interna di “crypto inventory” accanto ai documenti di sicurezza (ad esempio, /security).
Queste idee non sono limitate alle librerie crittografiche—si applicano a come costruisci e distribuisci software. Se usi un flusso di lavoro basato su generazione rapida (per esempio, Koder.ai, dove crei app web/server/mobile via chat), gli stessi principi appaiono come vincoli di prodotto: mantenere pochi stack supportati (React per il web, Go + PostgreSQL per il backend, Flutter per mobile), enfatizzare la pianificazione prima di generare cambiamenti e rendere economico il rollback.
Nella pratica, funzionalità come planning mode, snapshot e rollback ed esportazione del codice sorgente aiutano a ridurre il "raggio d’azione" degli errori: puoi rivedere l’intento prima che i cambiamenti atterrino, ripristinare rapidamente quando qualcosa va storto e verificare che ciò che gira corrisponda a ciò che è stato generato. È lo stesso istinto security-by-construction della compartimentazione di qmail—applicato alle pipeline moderne di delivery.
Security-by-construction significa progettare il software in modo che la via più sicura sia anche la più semplice. Invece di affidarsi alle persone che ricordino lunghe liste di controllo, si vincola il sistema in modo che gli errori comuni siano difficili da commettere e gli errori inevitabili abbiano un impatto limitato (raggio d’azione più piccolo).
La complessità crea interazioni nascoste e casi limite difficili da testare e facili da configurare male.
Vantaggi pratici della semplicità includono:
Un’interfaccia stretta fa meno e accetta meno variazioni. Evita input ambigui e riduce le modalità opzionali che creano una “sicurezza per configurazione”.
Un approccio pratico:
qmail suddivide la gestione della posta in piccoli programmi (ricevere, mettere in coda, consegnare, ecc.) con responsabilità ristrette. Questo riduce i modi di guasto perché:
Il comportamento constant-time mira a rendere il tempo di esecuzione (e spesso i pattern di accesso alla memoria) indipendenti dai valori segreti. Questo è importante perché gli attaccanti possono a volte inferire segreti misurando tempi, effetti sulla cache o differenze tra “fast path” e “slow path” su molte prove.
Si tratta di prevenire «fughe invisibili», non solo di scegliere algoritmi forti.
Identifica cosa è segreto (chiavi private, segreti condivisi, chiavi MAC, tag di autenticazione) e poi cerca posti dove i segreti influenzano il controllo di flusso o l’accesso alla memoria.
Segnali d’allarme:
if su dati segretiVerifica anche che la tua dipendenza crittografica dichiari esplicitamente comportamento constant-time per le operazioni che usi.
X25519 è una funzione specifica di scambio chiavi costruita su Curve25519. È popolare perché riduce i "foot-gun": meno parametri da scegliere, prestazioni solide e un design che favorisce implementazioni constant-time.
È una corsia di default più sicura per lo scambio di chiavi—purché tu gestisca correttamente autenticazione e gestione delle chiavi.
No. X25519 fornisce accordo di chiave (un segreto condiviso) ma non prova l’identità dell’altra parte.
Per prevenire l’usurpazione, abbinalo a meccanismi di autenticazione come:
Senza autenticazione, potresti comunque trovarsi a "parlare in modo sicuro" con la parte sbagliata.
NaCl riduce gli errori offrendo operazioni di alto livello già composte in modo sicuro, invece di esporre un buffet di algoritmi e modalità.
Costrutti comuni:
crypto_box: cifratura autenticata a chiave pubblica (tu + chiave del destinatario + nonce → ciphertext)crypto_secretbox: cifratura autenticata a chiave condivisaIl beneficio pratico è evitare errori di composizione comuni (come cifrare senza protezione d’integrità).
I buoni primitivi falliscono quando integrazione e operazioni sono approssimative. Problemi comuni:
Mitigazioni: