Go include una libreria standard ricca di strumenti crittografici, con API relativamente semplici e una forte attenzione alla sicurezza per impostazione predefinita. In questo articolo vediamo come usare in modo corretto e pragmatico i mattoni principali: generazione di numeri casuali sicuri, hashing e HMAC, derivazione di chiavi e password hashing, cifratura simmetrica con AES-GCM, cifratura asimmetrica con RSA-OAEP, firme digitali con ECDSA, e indicazioni operative su gestione delle chiavi e insidie comuni.
1. Principi base: cosa scegliere e quando
- Hash: impronta unidirezionale (integrità, deduplica, identificatori). Non è cifratura.
- HMAC: integrità e autenticazione con una chiave condivisa (token, webhook, API).
- Cifratura simmetrica: stessa chiave per cifrare/decifrare (dati a riposo, payload).
- Cifratura asimmetrica: coppia pubblica/privata (scambio chiavi, cifrare piccoli segreti).
- Firma digitale: autenticità e non ripudio (documenti, JWT firmati, update firmati).
Regola pratica: per dati usa cifratura simmetrica (es. AES-GCM) e conserva una chiave; per scambiare una chiave o firmare usa crittografia asimmetrica (RSA/ECDSA/Ed25519).
2. Random sicuro: crypto/rand (non math/rand)
Qualunque schema sicuro dipende da buona entropia. In Go, per chiavi, nonce e token usa sempre crypto/rand.
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
)
func main() {
// Token da 32 byte (256 bit)
token := make([]byte, 32)
if _, err := rand.Read(token); err != nil {
panic(err)
}
fmt.Println(hex.EncodeToString(token))
}
Per i nonce (AES-GCM) e i salt (password hashing), stessa regola: byte casuali da crypto/rand.
3. Hash: SHA-256 e SHA-512
L'hash serve per integrità o identificazione, ma non protegge la riservatezza: chiunque può calcolare lo stesso hash. Per integrità autenticata usa HMAC (sezione successiva).
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
)
func sha256Hex(b []byte) string {
sum := sha256.Sum256(b)
return hex.EncodeToString(sum[:])
}
func main() {
fmt.Println(sha256Hex([]byte("ciao mondo")))
}
4. HMAC: integrità + autenticazione con chiave condivisa
HMAC (con SHA-256, per esempio) è lo strumento tipico per firmare richieste API, verificare webhook o costruire token con integrità e autenticazione. L'idea: un hash che dipende da un segreto condiviso.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func hmacSHA256Hex(key, msg []byte) string {
mac := hmac.New(sha256.New, key)
mac.Write(msg)
return hex.EncodeToString(mac.Sum(nil))
}
func verifyHMACSHA256(key, msg []byte, expectedHex string) bool {
expected, err := hex.DecodeString(expectedHex)
if err != nil {
return false
}
mac := hmac.New(sha256.New, key)
mac.Write(msg)
actual := mac.Sum(nil)
// Confronto in tempo costante per evitare timing attacks
return hmac.Equal(actual, expected)
}
func main() {
key := []byte("chiave-super-segreta-da-ruotare")
msg := []byte("amount=100&to=alice")
tag := hmacSHA256Hex(key, msg)
fmt.Println("tag:", tag)
fmt.Println("ok:", verifyHMACSHA256(key, msg, tag))
}
Note operative
- Conserva la chiave HMAC in un secret manager e prevedi rotazione.
- Usa
hmac.Equalper confrontare tag/firmature. - Non riutilizzare una stessa chiave per scopi diversi (es. HMAC e cifratura).
5. Password: non SHA-256, ma un KDF (bcrypt/argon2/scrypt)
Per le password, un semplice hash (anche con salt) è troppo veloce e quindi vulnerabile a brute-force con GPU.
Serve un algoritmo lento e parametrizzabile: bcrypt, scrypt o Argon2.
In Go, molti di questi sono in golang.org/x/crypto.
Installazione (moduli):
go get golang.org/x/crypto/bcrypt
go get golang.org/x/crypto/argon2
bcrypt: semplice e diffuso
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func hashPassword(pw string) (string, error) {
// Cost tipico: 10-12. Valuta in base al tuo budget di CPU.
hash, err := bcrypt.GenerateFromPassword([]byte(pw), 12)
if err != nil {
return "", err
}
return string(hash), nil
}
func checkPassword(hash, pw string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) == nil
}
func main() {
h, _ := hashPassword("P@ssw0rd!")
fmt.Println("hash:", h)
fmt.Println("ok:", checkPassword(h, "P@ssw0rd!"))
}
Argon2id: scelta moderna (memoria-hard)
Argon2id è spesso consigliato perché resiste meglio ad attacchi con hardware specializzato. Qui mostriamo una struttura di output che include parametri e salt, così puoi verificare in futuro e migrare i parametri.
package main
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
type Argon2Params struct {
Time uint32
Memory uint32 // in KiB
Threads uint8
KeyLen uint32
}
func argon2idHash(pw string, p Argon2Params) (encoded string, err error) {
salt := make([]byte, 16)
if _, err = rand.Read(salt); err != nil {
return "", err
}
key := argon2.IDKey([]byte(pw), salt, p.Time, p.Memory, p.Threads, p.KeyLen)
// Formato semplice: argon2id$time$mem$threads$salt$hash (base64 raw, senza padding)
b64 := base64.RawStdEncoding
return fmt.Sprintf("argon2id$%d$%d$%d$%s$%s",
p.Time, p.Memory, p.Threads,
b64.EncodeToString(salt),
b64.EncodeToString(key),
), nil
}
func argon2idVerify(encoded, pw string, p Argon2Params) bool {
parts := strings.Split(encoded, "$")
if len(parts) != 6 || parts[0] != "argon2id" {
return false
}
// Qui, per brevità, assumiamo che i parametri coincidano con p.
// In produzione: parse dei parametri da parts[1..3] e confronto.
b64 := base64.RawStdEncoding
salt, err1 := b64.DecodeString(parts[4])
hash, err2 := b64.DecodeString(parts[5])
if err1 != nil || err2 != nil {
return false
}
key := argon2.IDKey([]byte(pw), salt, p.Time, p.Memory, p.Threads, uint32(len(hash)))
return subtle.ConstantTimeCompare(key, hash) == 1
}
func main() {
p := Argon2Params{Time: 2, Memory: 64 * 1024, Threads: 1, KeyLen: 32}
enc, _ := argon2idHash("P@ssw0rd!", p)
fmt.Println("encoded:", enc)
fmt.Println("ok:", argon2idVerify(enc, "P@ssw0rd!", p))
}
6. Derivazione di chiavi: HKDF per chiavi da un segreto principale
Quando hai un segreto principale (master secret) e vuoi derivare chiavi diverse per scopi diversi (cifratura, HMAC, ecc.), HKDF è una scelta solida. Evita di riutilizzare la stessa chiave per più funzioni.
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"golang.org/x/crypto/hkdf"
)
func hkdfKey(master, salt []byte, info string, n int) ([]byte, error) {
h := hkdf.New(sha256.New, master, salt, []byte(info))
out := make([]byte, n)
if _, err := io.ReadFull(h, out); err != nil {
return nil, err
}
return out, nil
}
func main() {
master := []byte("master-secret-da-gestire-con-cura")
salt := []byte("salt-per-contesto")
keyEnc, _ := hkdfKey(master, salt, "enc:v1", 32)
keyMac, _ := hkdfKey(master, salt, "mac:v1", 32)
fmt.Println("enc:", hex.EncodeToString(keyEnc))
fmt.Println("mac:", hex.EncodeToString(keyMac))
}
7. Cifratura simmetrica: AES-GCM (AEAD)
La scelta consigliata per la maggior parte dei casi è un modo AEAD (Authenticated Encryption with Associated Data), come AES-GCM. Offre riservatezza + integrità: se un attaccante modifica il ciphertext, la decifratura fallisce.
Regole importanti
- Usa una chiave AES da 16, 24 o 32 byte (AES-128/192/256).
- Usa un nonce unico per ogni cifratura con la stessa chiave (in GCM tipicamente 12 byte).
- Non reinventare formati: conserva
nonce || ciphertext(il tag è incluso nel ciphertext di GCM). - Puoi aggiungere Associated Data (AAD) non cifrati ma autenticati (es. userID, versione, header).
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
)
func encryptAESGCM(key, plaintext, aad []byte) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return "", err
}
// ciphertext include anche il tag di autenticazione
ciphertext := gcm.Seal(nil, nonce, plaintext, aad)
// output: nonce || ciphertext, base64 raw
out := append(nonce, ciphertext...)
return base64.RawStdEncoding.EncodeToString(out), nil
}
func decryptAESGCM(key []byte, token string, aad []byte) ([]byte, error) {
raw, err := base64.RawStdEncoding.DecodeString(token)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(raw) < nonceSize {
return nil, fmt.Errorf("ciphertext troppo corto")
}
nonce, ciphertext := raw[:nonceSize], raw[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, aad)
if err != nil {
return nil, err // include errori di autenticazione
}
return plaintext, nil
}
func main() {
key := make([]byte, 32) // AES-256
if _, err := rand.Read(key); err != nil {
panic(err)
}
aad := []byte("user=42;v=1")
token, _ := encryptAESGCM(key, []byte("segreto"), aad)
fmt.Println("token:", token)
pt, err := decryptAESGCM(key, token, aad)
fmt.Println("pt:", string(pt), "err:", err)
}
8. Cifratura asimmetrica: RSA-OAEP per piccoli segreti
RSA è utile per cifrare piccoli messaggi (ad esempio una chiave simmetrica) o per interoperabilità. Per la cifratura, preferisci OAEP (non PKCS#1 v1.5). Ricorda che RSA ha limiti di dimensione: il messaggio deve essere più corto della chiave.
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"fmt"
)
func main() {
// Generazione chiavi (in produzione: carica da file/secret manager, non rigenerare ogni avvio)
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
pub := &priv.PublicKey
label := []byte("ctx:v1")
msg := []byte("chiave-simmetrica-o-segreto-corto")
ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pub, msg, label)
if err != nil {
panic(err)
}
token := base64.RawStdEncoding.EncodeToString(ciphertext)
fmt.Println("token:", token)
raw, _ := base64.RawStdEncoding.DecodeString(token)
plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, raw, label)
fmt.Println("pt:", string(plaintext), "err:", err)
}
9. Firme digitali: ECDSA (e nota su Ed25519)
Una firma prova che un messaggio proviene dal possessore della chiave privata e che non è stato alterato. In Go puoi usare ECDSA (curve P-256, P-384, P-521) oppure Ed25519 (spesso più semplice e robusto come scelta moderna). Qui mostriamo ECDSA per familiarità con standard diffusi.
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/asn1"
"encoding/base64"
"fmt"
)
type ecdsaSig struct {
R, S *big.Int
}
func main() {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
pub := &priv.PublicKey
msg := []byte("documento da firmare")
digest := sha256.Sum256(msg)
r, s, err := ecdsa.Sign(rand.Reader, priv, digest[:])
if err != nil {
panic(err)
}
der, _ := asn1.Marshal(ecdsaSig{R: r, S: s})
sig := base64.RawStdEncoding.EncodeToString(der)
fmt.Println("sig:", sig)
raw, _ := base64.RawStdEncoding.DecodeString(sig)
var parsed ecdsaSig
if _, err := asn1.Unmarshal(raw, &parsed); err != nil {
panic(err)
}
ok := ecdsa.Verify(pub, digest[:], parsed.R, parsed.S)
fmt.Println("ok:", ok)
}
Nota: l'esempio sopra usa codifica ASN.1/DER per la coppia (R,S), utile per interoperabilità.
Per nuovi sistemi, valuta crypto/ed25519 perché evita molte delle complessità tipiche di ECDSA
(formato firma, nonce, ecc.).
10. TLS in Go: HTTPS e configurazioni pratiche
Se il tuo obiettivo è proteggere traffico di rete, spesso la risposta migliore è usare TLS e non costruire protocolli ad hoc. Go rende semplice esporre un server HTTPS.
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
// Certificati: tipicamente da Let's Encrypt o da un'infrastruttura interna.
log.Fatal(http.ListenAndServeTLS(":8443", "server.crt", "server.key", mux))
}
Per client HTTP, Go verifica i certificati per default. Evita di disabilitare la verifica (es. InsecureSkipVerify)
se non in casi estremi e controllati.
11. Gestione delle chiavi: dove si vince o si perde
Anche la miglior crittografia fallisce se le chiavi sono gestite male. Alcune pratiche consigliate:
- Secret manager: conserva chiavi e segreti fuori dal codice e fuori dai file di configurazione in chiaro.
- Rotazione: usa versioni (key id) e pianifica rotazioni; supporta più chiavi attive per migrazioni.
- Separazione: chiavi diverse per scopi diversi (enc vs mac vs firma).
- Minimo privilegio: limita accesso alle chiavi (processi, ruoli, policy).
- Audit: logga accessi alle chiavi e operazioni crittografiche sensibili dove possibile.
12. Insidie comuni (e come evitarle)
- Nonce riutilizzato in GCM: è uno degli errori più gravi. Assicurati di generare nonce unici.
- DIY crypto: evita di inventare schemi o combinazioni non standard.
- Confronti non costanti: per token/firmature usa confronti in tempo costante (
hmac.Equal,subtle.ConstantTimeCompare). - Chiavi nel repository: mai committare chiavi private, nemmeno "temporaneamente".
- Hash per password: usa bcrypt/scrypt/argon2, non SHA-256.
- RSA per grandi dati: usa RSA per cifrare chiavi, non file o payload grandi.
13. Mini-checklist per un progetto reale
| Obiettivo | Scelta consigliata | Note |
|---|---|---|
| Password utenti | Argon2id o bcrypt | Parametri calibrati sul tuo hardware; includi salt e versione. |
| Integrità richieste | HMAC-SHA-256 | Chiave ruotabile; confronto in tempo costante. |
| Dati a riposo | AES-GCM | Nonce unico; conserva nonce con il ciphertext; usa AAD per metadati. |
| Scambio chiavi | RSA-OAEP o ECDH | RSA per segreti piccoli; per protocolli moderni preferisci ECDH via TLS. |
| Firme | Ed25519 o ECDSA P-256 | Ed25519 spesso più semplice; ECDSA per compatibilità. |
Conclusione
Con Go puoi implementare facilmente componenti crittografici robusti, ma la sicurezza dipende dalle scelte: usa primitive moderne e standard, proteggi e ruota le chiavi, e preferisci protocolli esistenti (TLS) quando l'obiettivo è la comunicazione sicura. Se devi progettare un formato o un protocollo nuovo, riduci al minimo le parti "creative" e appoggiati a schemi AEAD, KDF riconosciuti e librerie ben mantenute.