425 lines
9.7 KiB
Markdown
425 lines
9.7 KiB
Markdown
# Cryptography Security Rules
|
|
|
|
Cryptography vulnerabilities threaten confidentiality and integrity of sensitive data.
|
|
|
|
**Rules:**
|
|
|
|
1. TLS MUST use 1.2+.
|
|
2. NEVER use DES, RC4, MD5, or SHA1 for security purposes.
|
|
3. SSH host keys MUST be verified — NEVER use `InsecureIgnoreHostKey`.
|
|
4. Passwords MUST be hashed with Argon2id (preferred) or bcrypt.
|
|
5. Security-critical randomness MUST use `crypto/rand`.
|
|
|
|
---
|
|
|
|
## Algorithm Selection Guide
|
|
|
|
Choose the right algorithm for the job — using the wrong primitive (e.g. SHA256 for passwords) is as dangerous as using a broken one:
|
|
|
|
| Use Case | Recommended | Avoid | Why |
|
|
| --- | --- | --- | --- |
|
|
| Symmetric encryption | AES-256-GCM, ChaCha20-Poly1305 | DES, 3DES, AES-ECB, RC4 | ECB reveals patterns; DES/RC4 are broken |
|
|
| Password hashing | Argon2id (preferred), bcrypt, scrypt | MD5, SHA-1, plain SHA-256 | Fast hashes enable brute-force; memory-hard functions resist GPU attacks |
|
|
| Message authentication | HMAC-SHA256, Poly1305 | HMAC-MD5, HMAC-SHA1 | MD5/SHA1 have known collision weaknesses |
|
|
| Digital signatures | Ed25519, ECDSA P-256 | RSA-PKCS1v1.5 | PKCS1v1.5 has padding oracle vulnerabilities |
|
|
| Key exchange | X25519, ECDH P-256 | Static RSA key transport | Forward secrecy requires ephemeral keys |
|
|
| Random generation | `crypto/rand` | `math/rand` | `math/rand` output is predictable |
|
|
| TLS | TLS 1.2+ (prefer 1.3) | TLS 1.0, 1.1, SSL | Known attacks (BEAST, POODLE) on older versions |
|
|
|
|
### Key Size Requirements
|
|
|
|
| Algorithm | Minimum Key Size | Recommended |
|
|
| --------- | ------------------------ | ---------------- |
|
|
| RSA | 2048 bits | 4096 bits |
|
|
| AES | 128 bits | 256 bits |
|
|
| ECDSA | P-256 (128-bit security) | P-256 or Ed25519 |
|
|
|
|
---
|
|
|
|
## Key Rotation Pattern
|
|
|
|
Keys should be rotated periodically. Use envelope encryption so rotating the Key Encryption Key (KEK) doesn't require re-encrypting all data:
|
|
|
|
```go
|
|
// Envelope encryption: encrypt data with a DEK, encrypt DEK with KEK
|
|
func EnvelopeEncrypt(kek, plaintext []byte) (encryptedDEK, ciphertext []byte, err error) {
|
|
// 1. Generate random Data Encryption Key
|
|
dek := make([]byte, 32)
|
|
if _, err := rand.Read(dek); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// 2. Encrypt data with DEK
|
|
ciphertext, err = EncryptAESGCM(dek, plaintext)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// 3. Encrypt DEK with KEK
|
|
encryptedDEK, err = EncryptAESGCM(kek, dek)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return encryptedDEK, ciphertext, nil
|
|
}
|
|
|
|
func EnvelopeDecrypt(kek, encryptedDEK, ciphertext []byte) ([]byte, error) {
|
|
dek, err := DecryptAESGCM(kek, encryptedDEK)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return DecryptAESGCM(dek, ciphertext)
|
|
}
|
|
|
|
func EncryptAESGCM(key, plaintext []byte) ([]byte, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil { return nil, err }
|
|
aead, err := cipher.NewGCM(block)
|
|
if err != nil { return nil, err }
|
|
nonce := make([]byte, aead.NonceSize())
|
|
if _, err := rand.Read(nonce); err != nil { return nil, err }
|
|
return aead.Seal(nonce, nonce, plaintext, nil), nil
|
|
}
|
|
|
|
func DecryptAESGCM(key, ciphertext []byte) ([]byte, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil { return nil, err }
|
|
aead, err := cipher.NewGCM(block)
|
|
if err != nil { return nil, err }
|
|
nonceSize := aead.NonceSize()
|
|
if len(ciphertext) < nonceSize {
|
|
return nil, errors.New("ciphertext too short")
|
|
}
|
|
return aead.Open(nil, ciphertext[:nonceSize], ciphertext[nonceSize:], nil)
|
|
}
|
|
```
|
|
|
|
When the KEK is rotated, only re-encrypt the DEKs (small), not the data (potentially large).
|
|
|
|
---
|
|
|
|
## Common Cryptographic Mistakes
|
|
|
|
### Mistake 1: AES-ECB reveals patterns — High
|
|
|
|
ECB encrypts each block independently — identical plaintext blocks produce identical ciphertext blocks, revealing data structure:
|
|
|
|
```go
|
|
// Bad — ECB mode reveals patterns in structured data
|
|
block, _ := aes.NewCipher(key)
|
|
// Using block.Encrypt directly = ECB mode
|
|
|
|
// Good — GCM provides authenticated encryption
|
|
aead, err := cipher.NewGCM(block) // randomized, authenticated
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nonce := make([]byte, aead.NonceSize())
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
return nil, err
|
|
}
|
|
ciphertext := aead.Seal(nonce, nonce, plaintext, nil)
|
|
```
|
|
|
|
### Mistake 2: Reusing nonces — Critical
|
|
|
|
A nonce reuse with AES-GCM completely breaks confidentiality and authentication:
|
|
|
|
```go
|
|
// Bad — static or reused nonce
|
|
nonce := []byte("fixed_nonce!") // catastrophic with GCM
|
|
|
|
// Good — random nonce per encryption
|
|
nonce := make([]byte, 12) // 96-bit for GCM
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
return nil, err
|
|
}
|
|
```
|
|
|
|
### Mistake 3: Non-constant-time comparison for secrets — Medium
|
|
|
|
Comparing secrets with `==` short-circuits on the first differing byte, leaking timing information. See [Network/Web Security — Observable Timing](./network.md) for constant-time comparison patterns using `crypto/subtle`.
|
|
|
|
---
|
|
|
|
## Insecure TLS Configuration — High
|
|
|
|
Using insecure TLS configurations can expose your application to man-in-the-middle attacks.
|
|
|
|
**Bad:**
|
|
|
|
```go
|
|
transport := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: true, // DON'T: verify certificates
|
|
},
|
|
}
|
|
```
|
|
|
|
**Good:**
|
|
|
|
```go
|
|
import "crypto/tls"
|
|
|
|
func secureConfig() *tls.Config {
|
|
return &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## DES Encryption — High
|
|
|
|
DES is cryptographically broken.
|
|
|
|
**Bad:**
|
|
|
|
```go
|
|
import "crypto/des"
|
|
block, _ := des.NewCipher(key) // DON'T: broken
|
|
```
|
|
|
|
**Good:**
|
|
|
|
```go
|
|
import "crypto/aes"
|
|
block, _ := aes.NewCipher(key) // OK: AES
|
|
cipher.NewGCM(block) // OK: GCM for auth
|
|
```
|
|
|
|
---
|
|
|
|
## Insecure SSH Host Key Verification — High
|
|
|
|
**Bad:**
|
|
|
|
```go
|
|
import "golang.org/x/crypto/ssh"
|
|
&ssh.ClientConfig{
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // DON'T
|
|
}
|
|
```
|
|
|
|
**Good:**
|
|
|
|
```go
|
|
import "golang.org/x/crypto/ssh"
|
|
&ssh.ClientConfig{
|
|
HostKeyCallback: ssh.FixedHostKey(publicKey),
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## MD5 Hash — High
|
|
|
|
MD5 is collision-prone and weak for security.
|
|
|
|
**Bad:**
|
|
|
|
```go
|
|
import "crypto/md5"
|
|
hash := md5.Sum([]byte(data)) // DON'T: weak
|
|
```
|
|
|
|
**Good:**
|
|
|
|
```go
|
|
// For password hashing:
|
|
import "golang.org/x/crypto/argon2"
|
|
hash := argon2.IDKey([]byte(pw), salt, 3, 64*1024, 4, 32)
|
|
|
|
// Or bcrypt (simpler API, no salt management):
|
|
import "golang.org/x/crypto/bcrypt"
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// For general-purpose hashing (not passwords):
|
|
import "crypto/sha256"
|
|
digest := sha256.Sum256(data)
|
|
```
|
|
|
|
---
|
|
|
|
## RC4 Cipher — High
|
|
|
|
RC4 is cryptographically broken.
|
|
|
|
**Bad:**
|
|
|
|
```go
|
|
import "crypto/rc4"
|
|
cipher, _ := rc4.NewCipher(key) // DON'T: broken
|
|
```
|
|
|
|
**Good:**
|
|
|
|
```go
|
|
import "crypto/cipher"
|
|
import "crypto/aes"
|
|
aead, _ := cipher.NewGCM(block) // OK: AES-GCM
|
|
|
|
// Or ChaCha20:
|
|
import "golang.org/x/crypto/chacha20poly1305"
|
|
aead, _ := chacha20poly1305.New(key)
|
|
```
|
|
|
|
---
|
|
|
|
## SHA1 Hash — Medium
|
|
|
|
SHA1 provides insufficient collision resistance.
|
|
|
|
**Bad:**
|
|
|
|
```go
|
|
import "crypto/sha1"
|
|
hash := sha1.Sum(data) // DON'T: weak
|
|
```
|
|
|
|
**Good:**
|
|
|
|
```go
|
|
import "crypto/sha256"
|
|
hash := sha256.Sum256(data)
|
|
```
|
|
|
|
---
|
|
|
|
## Weak Cryptographic Algorithms — Medium
|
|
|
|
**Bad:**
|
|
|
|
```go
|
|
import "crypto/hmac"
|
|
import "crypto/md5"
|
|
mac := hmac.New(md5.New, key) // DON'T: HMAC-MD5
|
|
```
|
|
|
|
**Good:**
|
|
|
|
```go
|
|
import "crypto/sha256"
|
|
mac := hmac.New(sha256.New, key)
|
|
```
|
|
|
|
---
|
|
|
|
## Insufficient Key Strength — Medium
|
|
|
|
RSA keys smaller than 2048 bits are insufficient.
|
|
|
|
**Bad:**
|
|
|
|
```go
|
|
import "crypto/rsa"
|
|
key, _ := rsa.GenerateKey(rand.Reader, 1024) // DON'T: too weak
|
|
```
|
|
|
|
**Good:**
|
|
|
|
```go
|
|
key, _ := rsa.GenerateKey(rand.Reader, 4096) // OK: 2048+ bits
|
|
```
|
|
|
|
---
|
|
|
|
## Weak Random Number Generators — High
|
|
|
|
`math/rand` is predictable, never use for security.
|
|
|
|
**Bad:**
|
|
|
|
```go
|
|
import "math/rand"
|
|
bytes := make([]byte, 16)
|
|
rand.Read(bytes) // DON'T: predictable
|
|
```
|
|
|
|
**Good:**
|
|
|
|
```go
|
|
import "crypto/rand"
|
|
_, err := rand.Read(bytes) // OK: cryptographically secure
|
|
```
|
|
|
|
---
|
|
|
|
## Weak TLS Versions — High
|
|
|
|
TLS 1.0 and 1.1 have known vulnerabilities.
|
|
|
|
**Bad:**
|
|
|
|
```go
|
|
import "crypto/tls"
|
|
&tls.Config{MinVersion: tls.VersionTLS10} // DON'T
|
|
```
|
|
|
|
**Good:**
|
|
|
|
```go
|
|
&tls.Config{MinVersion: tls.VersionTLS12} // OK
|
|
```
|
|
|
|
---
|
|
|
|
## Password Hashing — High
|
|
|
|
Don't use MD5, SHA1, or single-iteration hashes for passwords.
|
|
|
|
**Bad:**
|
|
|
|
```go
|
|
import "crypto/sha256"
|
|
hash := sha256.Sum256([]byte(password)) // DON'T: too fast
|
|
```
|
|
|
|
**Good:**
|
|
|
|
```go
|
|
// Argon2id (preferred) — memory-hard, resists GPU attacks:
|
|
import "golang.org/x/crypto/argon2"
|
|
key := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, 32)
|
|
|
|
// Or bcrypt (simpler API, widely supported):
|
|
import "golang.org/x/crypto/bcrypt"
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Or PBKDF2 with 600,000+ iterations (Go 1.24+ stdlib):
|
|
import "crypto/pbkdf2"
|
|
key, err := pbkdf2.Key(sha512.New, password, salt, 600_000, 32)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Or scrypt:
|
|
import "golang.org/x/crypto/scrypt"
|
|
key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
```
|
|
|
|
For Go 1.24+, prefer stdlib `crypto/hkdf`, `crypto/pbkdf2`, and `crypto/sha3`. Use `golang.org/x/crypto/...` fallbacks only for modules targeting older Go versions or for algorithms still outside the standard library.
|
|
|
|
---
|
|
|
|
## CWE References
|
|
|
|
- **CWE-327**: Use of a Broken or Risky Cryptographic Algorithm
|
|
- **CWE-331**: Insufficient Entropy
|
|
- **CWE-326**: Inadequate Encryption Strength
|
|
- **CWE-295**: Improper Certificate Validation
|
|
- **CWE-330**: Use of Insufficiently Random Values
|
|
- **CWE-916**: Use of Password Hash With Insufficient Computational Effort
|