Crittografia con Go

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.Equal per 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:

  1. Secret manager: conserva chiavi e segreti fuori dal codice e fuori dai file di configurazione in chiaro.
  2. Rotazione: usa versioni (key id) e pianifica rotazioni; supporta più chiavi attive per migrazioni.
  3. Separazione: chiavi diverse per scopi diversi (enc vs mac vs firma).
  4. Minimo privilegio: limita accesso alle chiavi (processi, ruoli, policy).
  5. 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.

Torna su