Cryptography

Think of cryptography as the digital equivalent of locked boxes and sealed envelopes for your data. Just as you wouldn't send sensitive information on a postcard for anyone to read, cryptography ensures that only authorized parties can access your digital communications. Go's crypto package provides a comprehensive toolkit of cryptographic building blocks that help you create secure applications, from simple password protection to complex encrypted communications.

In this modern digital age, understanding cryptography isn't just for security experts—it's essential for every developer. Whether you're building a web service, mobile app, or command-line tool, you'll inevitably need to protect user data, verify message authenticity, or secure communications between services. Go makes this accessible with well-designed APIs that make it difficult to use cryptography incorrectly.

💡 Key Takeaway: Cryptography is like a toolbox for securing data. Each tool has specific purposes—hashes for data integrity, encryption for confidentiality, and signatures for authenticity.

Understanding Cryptographic Fundamentals

Before diving into code, let's establish a solid foundation of cryptographic concepts. Understanding the "why" behind each cryptographic primitive helps you make better security decisions in your applications.

The CIA Triad

Information security revolves around three core principles, known as the CIA triad:

  1. Confidentiality: Ensuring that only authorized parties can access sensitive information. Encryption provides confidentiality by transforming readable data into ciphertext that appears as random noise to anyone without the decryption key.

  2. Integrity: Guaranteeing that data hasn't been tampered with during storage or transmission. Hash functions and digital signatures verify integrity by detecting even the smallest modifications to data.

  3. Availability: Ensuring that systems and data remain accessible to authorized users when needed. While not directly provided by cryptography, secure authentication and authorization mechanisms help maintain availability by preventing unauthorized access and denial-of-service attacks.

Cryptographic Algorithms: A Historical Perspective

Cryptography has evolved dramatically over the centuries. Ancient ciphers like the Caesar cipher (shifting letters by a fixed amount) could be broken by hand in minutes. Modern cryptographic algorithms rely on mathematical problems that would take supercomputers billions of years to solve through brute force.

Symmetric vs Asymmetric Cryptography:

The fundamental divide in cryptography is between symmetric and asymmetric algorithms. Symmetric cryptography uses the same key for encryption and decryption—fast and efficient, but you need a secure way to share the key. Asymmetric cryptography uses different keys for encryption (public key) and decryption (private key)—solves the key distribution problem but is much slower.

Think of symmetric encryption like a traditional padlock: everyone who needs access requires a copy of the same key. Asymmetric encryption is like a mailbox: anyone can drop mail in (using the public key), but only you can open it (with your private key).

The strongest encryption algorithm is useless if your keys are compromised. In production systems, key management is often more important than choosing the right algorithm. Consider these key management principles:

  • Key Generation: Always use cryptographically secure random number generators. Never use predictable seeds or patterns.
  • Key Storage: Store keys separately from the data they protect. Use hardware security modules (HSMs) or key management services for production systems.
  • Key Rotation: Regularly change encryption keys, even if you don't suspect compromise. This limits the damage if a key is eventually discovered.
  • Key Derivation: When deriving keys from passwords, use slow, memory-hard functions like Argon2 or scrypt to resist brute-force attacks.

Hashing

Think of hashing as creating a digital fingerprint for your data. Just as each person has unique fingerprints, each piece of data has a unique hash value. Hash functions are one-way streets—you can create a hash from data, but you can't reconstruct the original data from its hash. This makes them perfect for verifying data integrity and storing sensitive information like passwords.

Hash Function Properties

A cryptographic hash function must satisfy several critical properties:

  1. Deterministic: The same input always produces the same hash output
  2. Quick Computation: Fast to compute the hash for any input
  3. Avalanche Effect: Changing a single bit in the input completely changes the output
  4. One-way: Computationally infeasible to reverse the hash to get the original input
  5. Collision Resistant: Extremely difficult to find two different inputs that produce the same hash

These properties make hash functions ideal for data integrity verification, digital signatures, and password storage. Even a tiny change to the input data produces a completely different hash, making tampering immediately detectable.

Common Hash Functions

Let's start with the most fundamental hashing operations. These functions take any input data and produce a fixed-size output that uniquely represents that input.

 1package main
 2
 3import (
 4    "crypto/md5"
 5    "crypto/sha1"
 6    "crypto/sha256"
 7    "crypto/sha512"
 8    "encoding/hex"
 9    "fmt"
10)
11
12func main() {
13    data := []byte("Hello, World!")
14
15    // MD5 (128-bit hash - DEPRECATED for security)
16    md5Hash := md5.Sum(data)
17    fmt.Printf("MD5: %x\n", md5Hash)
18
19    // SHA1 (160-bit hash - DEPRECATED for security)
20    sha1Hash := sha1.Sum(data)
21    fmt.Printf("SHA1: %x\n", sha1Hash)
22
23    // SHA256 (256-bit hash - RECOMMENDED)
24    sha256Hash := sha256.Sum256(data)
25    fmt.Printf("SHA256: %x\n", sha256Hash)
26
27    // SHA512 (512-bit hash - RECOMMENDED)
28    sha512Hash := sha512.Sum512(data)
29    fmt.Printf("SHA512: %x\n", sha512Hash)
30
31    // Using hex encoding for human-readable format
32    encoded := hex.EncodeToString(sha256Hash[:])
33    fmt.Printf("SHA256 (hex): %s\n", encoded)
34
35    // Demonstrate avalanche effect
36    dataModified := []byte("Hello, World?")
37    sha256Modified := sha256.Sum256(dataModified)
38    fmt.Printf("\nOriginal:  %x\n", sha256Hash)
39    fmt.Printf("Modified:  %x\n", sha256Modified)
40    fmt.Println("Notice how changing one character completely changes the hash!")
41}
42// run

⚠️ Security Warning: MD5 and SHA1 are cryptographically broken and should not be used for security purposes. They're still acceptable for non-security applications like checksums, but use SHA-256 or SHA-512 for anything security-related.

Hash Algorithm Selection Guide

Choosing the right hash algorithm depends on your use case:

  • SHA-256: The industry standard for most applications. Excellent balance of security and performance. Use this unless you have specific reasons to choose otherwise.

  • SHA-512: Provides higher security margin and can be faster on 64-bit systems. Good choice for long-term data integrity where the larger hash size isn't a concern.

  • SHA-3: The latest SHA standard, based on a completely different design than SHA-2. Use when you need an alternative to SHA-2 for defense-in-depth.

  • BLAKE2: Faster than MD5 while being more secure than SHA-2. Excellent choice for checksums and hash tables where performance matters.

Streaming Hash Computation

When hashing large files or data streams, you don't want to load everything into memory at once. Go's hash interfaces support incremental updates:

 1package main
 2
 3import (
 4    "crypto/sha256"
 5    "fmt"
 6    "io"
 7    "strings"
 8)
 9
10func main() {
11    // Create hash instance
12    hasher := sha256.New()
13
14    // Stream data in chunks
15    data := strings.NewReader("This is a large piece of data that might come from a file or network stream")
16
17    // Read and hash in chunks
18    buffer := make([]byte, 16)
19    for {
20        n, err := data.Read(buffer)
21        if err == io.EOF {
22            break
23        }
24        hasher.Write(buffer[:n])
25    }
26
27    // Get final hash
28    hash := hasher.Sum(nil)
29    fmt.Printf("Streaming hash: %x\n", hash)
30
31    // Compare with direct hashing
32    directHash := sha256.Sum256([]byte("This is a large piece of data that might come from a file or network stream"))
33    fmt.Printf("Direct hash:    %x\n", directHash)
34    fmt.Printf("Hashes match: %v\n", string(hash) == string(directHash[:]))
35}
36// run

HMAC

⚠️ Important: While basic hash functions verify data integrity, they don't prevent tampering. Anyone can modify the data and create a new hash. HMAC solves this by adding a secret key—like sealing an envelope with wax that only you have.

HMAC is like a tamper-evident seal for your messages. It combines the message with a secret key before hashing, ensuring that only someone with the same secret key could have created the hash. This makes it perfect for verifying that messages haven't been altered in transit.

HMAC Use Cases:

  • API request signing (AWS, webhook verification)
  • Cookie integrity verification
  • Message authentication in protocols
  • Preventing timing attacks with constant-time comparison
 1package main
 2
 3import (
 4    "crypto/hmac"
 5    "crypto/sha256"
 6    "encoding/hex"
 7    "fmt"
 8)
 9
10func generateHMAC(message, key []byte) string {
11    h := hmac.New(sha256.New, key)
12    h.Write(message)
13    return hex.EncodeToString(h.Sum(nil))
14}
15
16func verifyHMAC(message, key []byte, expectedMAC string) bool {
17    actualMAC := generateHMAC(message, key)
18    // Use hmac.Equal for constant-time comparison (prevents timing attacks)
19    return hmac.Equal([]byte(actualMAC), []byte(expectedMAC))
20}
21
22func main() {
23    key := []byte("my-secret-key")
24    message := []byte("important message")
25
26    // Generate HMAC
27    mac := generateHMAC(message, key)
28    fmt.Printf("HMAC: %s\n", mac)
29
30    // Verify HMAC
31    valid := verifyHMAC(message, key, mac)
32    fmt.Printf("Valid: %v\n", valid)
33
34    // Tampered message
35    tamperedMessage := []byte("tampered message")
36    validTampered := verifyHMAC(tamperedMessage, key, mac)
37    fmt.Printf("Tampered valid: %v\n", validTampered)
38
39    // Wrong key
40    wrongKey := []byte("wrong-key")
41    validWrongKey := verifyHMAC(message, wrongKey, mac)
42    fmt.Printf("Wrong key valid: %v\n", validWrongKey)
43
44    fmt.Println("\nHMAC provides both integrity AND authenticity!")
45}
46// run

Why HMAC.Equal() Matters: The hmac.Equal() function performs constant-time comparison, meaning it takes the same amount of time regardless of where the first difference occurs. This prevents timing attacks where an attacker measures how long comparisons take to gradually guess the correct MAC.

Symmetric Encryption

Now we move from integrity to confidentiality. While hashing protects data from being altered, encryption protects it from being read. Symmetric encryption is like having a single key that both locks and unlocks a box—the same key is used to encrypt and decrypt data.

Understanding Block Ciphers and Modes

Symmetric encryption algorithms like AES are block ciphers—they encrypt data in fixed-size blocks (128 bits for AES). But what if your data isn't an exact multiple of the block size? And what if you're encrypting multiple blocks—can patterns in the plaintext reveal information?

This is where cipher modes come in. They define how to apply the block cipher to data of arbitrary length:

  • ECB (Electronic Codebook): ⚠️ Never use! Encrypts each block independently, which can leak patterns.
  • CBC (Cipher Block Chaining): Each block depends on the previous one. Requires an IV.
  • CTR (Counter): Turns block cipher into stream cipher. Parallelizable and fast.
  • GCM (Galois/Counter Mode): Authenticated encryption—provides both encryption and integrity verification. This is what you should use.

AES Encryption

AES is the gold standard for symmetric encryption. Think of it as a high-tech vault that uses complex mathematical operations to scramble your data into unreadable ciphertext. Only someone with the exact same key can unscramble it back to the original plaintext.

AES Key Sizes:

  • AES-128: 128-bit key (16 bytes) - Fast and secure for most applications
  • AES-192: 192-bit key (24 bytes) - Additional security margin
  • AES-256: 256-bit key (32 bytes) - Maximum security for highly sensitive data

Real-world Example

When you send a secure message through WhatsApp or Signal, they use AES encryption to ensure that only you and the recipient can read the message. Even if someone intercepts the data, it looks like random noise without the key.

 1package main
 2
 3import (
 4    "crypto/aes"
 5    "crypto/cipher"
 6    "crypto/rand"
 7    "encoding/hex"
 8    "fmt"
 9    "io"
10)
11
12func encrypt(plaintext, key []byte) ([]byte, error) {
13    block, err := aes.NewCipher(key)
14    if err != nil {
15        return nil, err
16    }
17
18    // Create GCM mode - provides authenticated encryption
19    gcm, err := cipher.NewGCM(block)
20    if err != nil {
21        return nil, err
22    }
23
24    // Generate nonce (number used once)
25    // CRITICAL: Never reuse a nonce with the same key!
26    nonce := make([]byte, gcm.NonceSize())
27    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
28        return nil, err
29    }
30
31    // Encrypt and authenticate
32    // GCM appends the nonce to the ciphertext
33    ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
34    return ciphertext, nil
35}
36
37func decrypt(ciphertext, key []byte) ([]byte, error) {
38    block, err := aes.NewCipher(key)
39    if err != nil {
40        return nil, err
41    }
42
43    gcm, err := cipher.NewGCM(block)
44    if err != nil {
45        return nil, err
46    }
47
48    nonceSize := gcm.NonceSize()
49    if len(ciphertext) < nonceSize {
50        return nil, fmt.Errorf("ciphertext too short")
51    }
52
53    // Extract nonce and ciphertext
54    nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
55
56    // Decrypt and verify authentication
57    return gcm.Open(nil, nonce, ciphertext, nil)
58}
59
60func main() {
61    // Generate secure random key (32 bytes for AES-256)
62    key := make([]byte, 32)
63    if _, err := rand.Read(key); err != nil {
64        panic(err)
65    }
66
67    plaintext := []byte("Secret message that needs encryption!")
68    fmt.Printf("Original: %s\n", string(plaintext))
69
70    // Encrypt
71    ciphertext, err := encrypt(plaintext, key)
72    if err != nil {
73        panic(err)
74    }
75    fmt.Printf("Encrypted (%d bytes): %s\n", len(ciphertext), hex.EncodeToString(ciphertext[:40])+"...")
76
77    // Decrypt
78    decrypted, err := decrypt(ciphertext, key)
79    if err != nil {
80        panic(err)
81    }
82    fmt.Printf("Decrypted: %s\n", string(decrypted))
83
84    // Try with wrong key
85    wrongKey := make([]byte, 32)
86    rand.Read(wrongKey)
87    _, err = decrypt(ciphertext, wrongKey)
88    if err != nil {
89        fmt.Printf("\n✓ Decryption with wrong key failed: %v\n", err)
90    }
91}
92// run

Why GCM Mode?

GCM (Galois/Counter Mode) provides authenticated encryption, which means it provides both confidentiality and integrity in a single operation. This is crucial because:

  1. Prevents tampering: An attacker can't modify the ciphertext without detection
  2. Prevents bit-flipping attacks: Changing any bit causes decryption to fail
  3. Efficient: Uses parallel operations for high performance
  4. Standard: Widely used in TLS 1.3, IPsec, and other protocols

Encryption Best Practices

  1. Never reuse nonces: Each encryption operation must use a unique nonce. Nonce reuse with GCM completely breaks security.

  2. Store nonces with ciphertext: The nonce doesn't need to be secret, so prepending it to the ciphertext is standard practice.

  3. Use authenticated encryption: Always use modes like GCM that provide both encryption and authentication. Never use encryption without integrity verification.

  4. Generate keys properly: Use crypto/rand for all key generation. Never derive keys from predictable sources.

  5. Rotate keys regularly: Have a key rotation strategy. Don't use the same encryption key forever.

Asymmetric Encryption

What if you want to communicate with someone you've never met before? How do you securely share the encryption key? This is where asymmetric encryption comes in—it's like having a mailbox with two different keys: a public key that anyone can use to drop mail in, and a private key that only you have to open it.

Public Key Cryptography Concepts

Asymmetric cryptography relies on mathematical trapdoor functions—operations that are easy in one direction but hard in reverse. For example:

  • RSA: Based on the difficulty of factoring large numbers
  • ECDSA: Based on the discrete logarithm problem on elliptic curves
  • EdDSA: Modern variant with better performance and security

The brilliance of public key cryptography is that you can freely distribute your public key without compromising security. Anyone can encrypt messages to you, but only you can decrypt them.

RSA Key Generation and Usage

RSA encryption revolutionized secure communications by solving the key distribution problem. Think of your public key as your bank account number—you can share it with anyone who wants to send you money, but only your private key can access those funds.

When to Use Symmetric vs Asymmetric

  • Symmetric: Fast, efficient for large amounts of data, same key for encryption/decryption
  • Asymmetric: Slower, perfect for key exchange and digital signatures, different keys for encryption/decryption

In practice, systems often use both: RSA to securely exchange AES keys, then AES for the actual data encryption. This hybrid approach gives you the best of both worlds.

 1package main
 2
 3import (
 4    "crypto/rand"
 5    "crypto/rsa"
 6    "crypto/sha256"
 7    "crypto/x509"
 8    "encoding/pem"
 9    "fmt"
10)
11
12func generateRSAKeys() (*rsa.PrivateKey, *rsa.PublicKey, error) {
13    // Generate 2048-bit RSA key pair
14    // For higher security, use 4096 bits (but slower)
15    privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
16    if err != nil {
17        return nil, nil, err
18    }
19    return privateKey, &privateKey.PublicKey, nil
20}
21
22func encryptRSA(publicKey *rsa.PublicKey, plaintext []byte) ([]byte, error) {
23    // Use OAEP padding (Optimal Asymmetric Encryption Padding)
24    // More secure than PKCS#1 v1.5
25    return rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, plaintext, nil)
26}
27
28func decryptRSA(privateKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error) {
29    return rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, ciphertext, nil)
30}
31
32func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte {
33    privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
34    privateKeyPEM := pem.EncodeToMemory(&pem.Block{
35        Type:  "RSA PRIVATE KEY",
36        Bytes: privateKeyBytes,
37    })
38    return privateKeyPEM
39}
40
41func encodePublicKeyToPEM(publicKey *rsa.PublicKey) ([]byte, error) {
42    publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
43    if err != nil {
44        return nil, err
45    }
46    publicKeyPEM := pem.EncodeToMemory(&pem.Block{
47        Type:  "PUBLIC KEY",
48        Bytes: publicKeyBytes,
49    })
50    return publicKeyPEM, nil
51}
52
53func main() {
54    // Generate keys
55    privateKey, publicKey, err := generateRSAKeys()
56    if err != nil {
57        panic(err)
58    }
59
60    fmt.Println("=== RSA Key Pair Generated ===")
61    fmt.Printf("Key size: %d bits\n", privateKey.Size()*8)
62
63    // Export keys to PEM format (standard for key storage)
64    privPEM := encodePrivateKeyToPEM(privateKey)
65    pubPEM, _ := encodePublicKeyToPEM(publicKey)
66
67    fmt.Println("\nPrivate Key (first 100 chars):")
68    fmt.Println(string(privPEM[:100]) + "...")
69    fmt.Println("\nPublic Key (first 100 chars):")
70    fmt.Println(string(pubPEM[:100]) + "...")
71
72    // Encrypt with public key
73    plaintext := []byte("Secret message")
74    fmt.Printf("\n=== Encryption ===\nPlaintext: %s\n", plaintext)
75
76    ciphertext, err := encryptRSA(publicKey, plaintext)
77    if err != nil {
78        panic(err)
79    }
80    fmt.Printf("Encrypted: %x...\n", ciphertext[:32])
81    fmt.Printf("Ciphertext size: %d bytes\n", len(ciphertext))
82
83    // Decrypt with private key
84    decrypted, err := decryptRSA(privateKey, ciphertext)
85    if err != nil {
86        panic(err)
87    }
88    fmt.Printf("\n=== Decryption ===\nDecrypted: %s\n", string(decrypted))
89
90    // Demonstrate RSA size limits
91    // RSA can only encrypt data smaller than the key size minus padding
92    maxSize := publicKey.Size() - 2*sha256.New().Size() - 2
93    fmt.Printf("\n=== RSA Limits ===\n")
94    fmt.Printf("Maximum plaintext size: %d bytes\n", maxSize)
95    fmt.Println("For larger data, use hybrid encryption (RSA + AES)")
96}
97// run

RSA Security Considerations

Key Size: RSA security depends heavily on key size. As computers get faster, key size requirements increase:

  • 1024 bits: ⚠️ Deprecated - can be broken
  • 2048 bits: Minimum for current security
  • 3072 bits: Recommended for long-term security
  • 4096 bits: High security, but slower operations

Padding Schemes: Always use OAEP padding, not PKCS#1 v1.5. OAEP provides better security against adaptive chosen-ciphertext attacks.

Performance: RSA is much slower than symmetric encryption. For large data, use hybrid encryption: generate a random AES key, encrypt data with AES, then encrypt the AES key with RSA.

Digital Signatures

Digital signatures solve the authentication problem—how do you know a message really came from who it claims to be? Think of digital signatures as digital handwriting that's impossible to forge. They use the mathematics of asymmetric encryption in reverse: instead of encrypting with a public key, you "encrypt" with a private key to create a signature that anyone can verify with your public key.

Signing and Verification

The beauty of digital signatures is that they provide three guarantees in one operation:

  1. Authentication: The message came from the private key holder
  2. Integrity: The message hasn't been tampered with
  3. Non-repudiation: The sender can't deny having sent the message

Real-world Example

When you sign a PDF document digitally, you're creating a digital signature. Anyone with your public certificate can verify that you signed it and that the document hasn't been changed since you signed it. This is legally binding in many countries.

Digital Signature Process:

  1. Hash the message to create a fixed-size digest
  2. Encrypt the hash with your private key to create the signature
  3. Recipient hashes the message and decrypts signature with your public key
  4. If the hashes match, the signature is valid
 1package main
 2
 3import (
 4    "crypto"
 5    "crypto/rand"
 6    "crypto/rsa"
 7    "crypto/sha256"
 8    "fmt"
 9)
10
11func signMessage(privateKey *rsa.PrivateKey, message []byte) ([]byte, error) {
12    // Hash the message
13    hash := sha256.Sum256(message)
14
15    // Sign the hash with private key
16    // PSS padding is more secure than PKCS#1 v1.5
17    return rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hash[:])
18}
19
20func verifySignature(publicKey *rsa.PublicKey, message, signature []byte) error {
21    // Hash the message
22    hash := sha256.Sum256(message)
23
24    // Verify the signature
25    return rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hash[:], signature)
26}
27
28func main() {
29    // Generate keys
30    privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
31    publicKey := &privateKey.PublicKey
32
33    message := []byte("This message needs to be signed")
34    fmt.Printf("Original message: %s\n", string(message))
35
36    // Sign the message
37    signature, err := signMessage(privateKey, message)
38    if err != nil {
39        panic(err)
40    }
41    fmt.Printf("\nSignature created (%d bytes): %x...\n", len(signature), signature[:32])
42
43    // Verify the signature
44    fmt.Println("\n=== Verification Tests ===")
45
46    err = verifySignature(publicKey, message, signature)
47    if err != nil {
48        fmt.Printf("✗ Verification failed: %v\n", err)
49    } else {
50        fmt.Println("✓ Signature verified successfully!")
51    }
52
53    // Try to verify tampered message
54    tamperedMessage := []byte("This message has been tampered with")
55    err = verifySignature(publicKey, tamperedMessage, signature)
56    if err != nil {
57        fmt.Printf("✓ Tampered message detected: %v\n", err)
58    } else {
59        fmt.Println("✗ Tampered message NOT detected!")
60    }
61
62    // Try to verify with wrong key
63    wrongPrivateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
64    wrongPublicKey := &wrongPrivateKey.PublicKey
65    err = verifySignature(wrongPublicKey, message, signature)
66    if err != nil {
67        fmt.Printf("✓ Wrong key detected: %v\n", err)
68    } else {
69        fmt.Println("✗ Wrong key NOT detected!")
70    }
71
72    fmt.Println("\n=== Use Cases ===")
73    fmt.Println("• Software distribution (code signing)")
74    fmt.Println("• Document workflows (legal contracts)")
75    fmt.Println("• API authentication (JWT signing)")
76    fmt.Println("• Blockchain transactions")
77}
78// run

Signature Algorithm Selection

Different signature algorithms offer different tradeoffs:

RSA Signatures:

  • Most widely supported
  • Signature size equals key size
  • Slower than ECDSA
  • Use PSS padding when possible

ECDSA (Elliptic Curve):

  • Much smaller keys and signatures
  • Faster than RSA
  • Increasingly common in modern systems
  • Good choice for mobile and IoT

EdDSA (Ed25519):

  • Fastest and most secure
  • Fixed 32-byte keys and 64-byte signatures
  • Resistant to timing attacks
  • Recommended for new systems

TLS Certificates

When you visit a website with HTTPS, how does your browser know it's really talking to google.com and not an impostor? TLS certificates are the digital ID cards of the internet. They bind a domain name to a public key, verified by a trusted certificate authority.

Understanding PKI (Public Key Infrastructure)

PKI is the system that makes TLS certificates trustworthy. It's based on a chain of trust:

  1. Root CAs: A small set of trusted certificate authorities built into your operating system and browsers
  2. Intermediate CAs: Certificate authorities trusted by root CAs, creating a chain
  3. End Entity Certificates: Your website's certificate, signed by an intermediate CA

When your browser connects to a website, it verifies the entire chain back to a trusted root CA. This hierarchical trust model allows the internet to scale while maintaining security.

Certificate Components

A TLS certificate contains several important pieces of information:

  • Subject: Who the certificate is issued to (domain name)
  • Issuer: Who signed the certificate (CA)
  • Validity Period: Start and end dates (typically 90-365 days)
  • Public Key: The public key for encrypting data to this server
  • Signature: The CA's digital signature proving authenticity
  • Extensions: Additional information like alternative names (SANs)

Self-Signed Certificate Generation

While production websites use certificates from trusted authorities like Let's Encrypt or DigiCert, self-signed certificates are perfect for development, testing, and internal services. Think of them as homemade ID cards—great for internal use, but not trusted by strangers.

Common Pitfalls with Certificates

  • Expired certificates: The most common TLS error - certificates have expiration dates
  • Domain mismatch: Certificate must exactly match the domain being accessed
  • Chain issues: Your browser needs to trust the certificate authority that signed your certificate
  • Weak keys: Use at least 2048-bit RSA or 256-bit ECDSA
  • Self-signed in production: Never use self-signed certificates for public-facing services

Real-world Example

When you visit https://yourbank.com, your browser checks that the certificate is for yourbank.com, that it's signed by a trusted authority, and that it hasn't expired. This entire process happens automatically in milliseconds, creating the secure connection that keeps your banking data safe.

 1package main
 2
 3import (
 4    "crypto/rand"
 5    "crypto/rsa"
 6    "crypto/x509"
 7    "crypto/x509/pkix"
 8    "encoding/pem"
 9    "fmt"
10    "math/big"
11    "time"
12)
13
14func generateSelfSignedCert() ([]byte, []byte, error) {
15    // Generate private key
16    privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
17    if err != nil {
18        return nil, nil, err
19    }
20
21    // Certificate template
22    template := x509.Certificate{
23        SerialNumber: big.NewInt(1),
24        Subject: pkix.Name{
25            Organization: []string{"My Company"},
26            CommonName:   "localhost",
27        },
28        NotBefore:             time.Now(),
29        NotAfter:              time.Now().Add(365 * 24 * time.Hour), // Valid for 1 year
30        KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
31        ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
32        BasicConstraintsValid: true,
33        DNSNames:              []string{"localhost", "*.localhost"},
34        IPAddresses:           nil, // Add IPs if needed
35    }
36
37    // Create certificate (self-signed, so template signs itself)
38    certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
39    if err != nil {
40        return nil, nil, err
41    }
42
43    // Encode certificate to PEM
44    certPEM := pem.EncodeToMemory(&pem.Block{
45        Type:  "CERTIFICATE",
46        Bytes: certDER,
47    })
48
49    // Encode private key to PEM
50    keyPEM := pem.EncodeToMemory(&pem.Block{
51        Type:  "RSA PRIVATE KEY",
52        Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
53    })
54
55    return certPEM, keyPEM, nil
56}
57
58func main() {
59    fmt.Println("=== Self-Signed TLS Certificate Generator ===\n")
60
61    certPEM, keyPEM, err := generateSelfSignedCert()
62    if err != nil {
63        panic(err)
64    }
65
66    fmt.Println("Certificate (PEM format):")
67    fmt.Println(string(certPEM))
68
69    fmt.Println("Private Key (PEM format - first 200 chars):")
70    fmt.Println(string(keyPEM[:200]) + "...")
71
72    // Parse and display certificate details
73    block, _ := pem.Decode(certPEM)
74    cert, err := x509.ParseCertificate(block.Bytes)
75    if err != nil {
76        panic(err)
77    }
78
79    fmt.Println("\n=== Certificate Details ===")
80    fmt.Printf("Subject: %s\n", cert.Subject.CommonName)
81    fmt.Printf("Issuer: %s\n", cert.Issuer.CommonName)
82    fmt.Printf("Valid From: %s\n", cert.NotBefore.Format(time.RFC3339))
83    fmt.Printf("Valid Until: %s\n", cert.NotAfter.Format(time.RFC3339))
84    fmt.Printf("DNS Names: %v\n", cert.DNSNames)
85    fmt.Printf("Serial Number: %s\n", cert.SerialNumber)
86    fmt.Printf("Signature Algorithm: %s\n", cert.SignatureAlgorithm)
87
88    fmt.Println("\n=== Usage ===")
89    fmt.Println("Save certificate to: server.crt")
90    fmt.Println("Save key to: server.key")
91    fmt.Println("Use with Go HTTP server:")
92    fmt.Println(`  http.ListenAndServeTLS(":443", "server.crt", "server.key", handler)`)
93}
94// run

Certificate Best Practices

  1. Automate renewal: Certificates expire. Use tools like certbot for Let's Encrypt automation.

  2. Use short validity periods: Modern certificates use 90-day validity. This limits the damage from compromised keys.

  3. Monitor expiration: Set up alerts at least 30 days before expiration.

  4. Include SANs: Use Subject Alternative Names to support multiple domains with one certificate.

  5. Proper storage: Store private keys with restricted permissions (chmod 600). Never commit them to version control.

JWT Tokens

JSON Web Tokens are like digital passports that prove your identity across different services. When you log into a website, instead of storing your session on their server, they give you a JWT that contains all the necessary information. Each time you make a request, you present this "passport" and the service can verify it without asking the server if you're still logged in.

JWT Structure

JWTs consist of three parts separated by dots: header.payload.signature

  1. Header: Metadata about the token (algorithm, type)

    1{"alg": "HS256", "typ": "JWT"}
    
  2. Payload: The actual data/claims (user ID, permissions, expiry)

    1{"user_id": 123, "exp": 1234567890}
    
  3. Signature: Cryptographic signature that prevents tampering

    HMAC-SHA256(base64(header) + "." + base64(payload), secret)
    

The beauty of JWTs is that they're stateless—the server doesn't need to remember anything about you. Your token contains everything needed to verify your identity, making them perfect for microservices and distributed systems.

JWT Creation and Validation

Common Pitfalls with JWTs

  • Never store sensitive data: JWTs are base64 encoded, not encrypted. Anyone can decode and read the payload.
  • Use short expiration times: If a JWT is stolen, it can be used until it expires. Keep it short (15 minutes).
  • Don't forget revocation: Implement a mechanism to invalidate tokens when needed (logout, password change).
  • Validate everything: Always verify signature, expiration, and issuer. Don't trust the client.

Real-world Example

When you use Google's "Sign in with Google" feature, Google issues a JWT to your application. Your application can verify this token using Google's public keys, confirming that you've successfully authenticated with Google without ever storing your password.

  1package main
  2
  3import (
  4    "crypto/hmac"
  5    "crypto/sha256"
  6    "encoding/base64"
  7    "encoding/json"
  8    "fmt"
  9    "strings"
 10    "time"
 11)
 12
 13type JWT struct {
 14    Header    map[string]interface{}
 15    Claims    map[string]interface{}
 16    Signature string
 17}
 18
 19func encodeSegment(data interface{}) (string, error) {
 20    jsonData, err := json.Marshal(data)
 21    if err != nil {
 22        return "", err
 23    }
 24    return base64.RawURLEncoding.EncodeToString(jsonData), nil
 25}
 26
 27func createJWT(claims map[string]interface{}, secret []byte) (string, error) {
 28    // Header
 29    header := map[string]interface{}{
 30        "alg": "HS256",
 31        "typ": "JWT",
 32    }
 33
 34    // Encode header and claims
 35    headerEncoded, _ := encodeSegment(header)
 36    claimsEncoded, _ := encodeSegment(claims)
 37
 38    // Create signature
 39    message := headerEncoded + "." + claimsEncoded
 40    h := hmac.New(sha256.New, secret)
 41    h.Write([]byte(message))
 42    signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
 43
 44    return message + "." + signature, nil
 45}
 46
 47func verifyJWT(token string, secret []byte) (map[string]interface{}, error) {
 48    parts := strings.Split(token, ".")
 49    if len(parts) != 3 {
 50        return nil, fmt.Errorf("invalid token format")
 51    }
 52
 53    // Verify signature
 54    message := parts[0] + "." + parts[1]
 55    h := hmac.New(sha256.New, secret)
 56    h.Write([]byte(message))
 57    expectedSig := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
 58
 59    if !hmac.Equal([]byte(expectedSig), []byte(parts[2])) {
 60        return nil, fmt.Errorf("invalid signature")
 61    }
 62
 63    // Decode claims
 64    claimsJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
 65    if err != nil {
 66        return nil, err
 67    }
 68
 69    var claims map[string]interface{}
 70    if err := json.Unmarshal(claimsJSON, &claims); err != nil {
 71        return nil, err
 72    }
 73
 74    // Check expiration
 75    if exp, ok := claims["exp"].(float64); ok {
 76        if time.Now().Unix() > int64(exp) {
 77            return nil, fmt.Errorf("token expired")
 78        }
 79    }
 80
 81    // Check not before
 82    if nbf, ok := claims["nbf"].(float64); ok {
 83        if time.Now().Unix() < int64(nbf) {
 84            return nil, fmt.Errorf("token not yet valid")
 85        }
 86    }
 87
 88    return claims, nil
 89}
 90
 91func main() {
 92    secret := []byte("my-secret-key")
 93
 94    // Create JWT
 95    claims := map[string]interface{}{
 96        "user_id":  12345,
 97        "username": "john_doe",
 98        "role":     "admin",
 99        "iat":      time.Now().Unix(),
100        "exp":      time.Now().Add(1 * time.Hour).Unix(),
101    }
102
103    token, err := createJWT(claims, secret)
104    if err != nil {
105        panic(err)
106    }
107    fmt.Printf("JWT Token:\n%s\n\n", token)
108
109    // Show token parts
110    parts := strings.Split(token, ".")
111    fmt.Println("=== Token Structure ===")
112    fmt.Printf("Header:    %s\n", parts[0])
113    fmt.Printf("Payload:   %s\n", parts[1])
114    fmt.Printf("Signature: %s\n", parts[2])
115
116    // Decode and show claims (anyone can do this!)
117    claimsJSON, _ := base64.RawURLEncoding.DecodeString(parts[1])
118    fmt.Printf("\n=== Decoded Payload (NOT encrypted!) ===\n%s\n", string(claimsJSON))
119
120    // Verify JWT
121    fmt.Println("\n=== Verification Tests ===")
122    verifiedClaims, err := verifyJWT(token, secret)
123    if err != nil {
124        fmt.Printf("✗ Verification failed: %v\n", err)
125    } else {
126        fmt.Println("✓ Signature verified successfully!")
127        fmt.Println("Verified claims:")
128        for k, v := range verifiedClaims {
129            fmt.Printf("  %s: %v\n", k, v)
130        }
131    }
132
133    // Test with wrong secret
134    wrongSecret := []byte("wrong-key")
135    _, err = verifyJWT(token, wrongSecret)
136    if err != nil {
137        fmt.Printf("\n✓ Wrong secret detected: %v\n", err)
138    }
139
140    // Test with tampered payload
141    tamperedToken := parts[0] + ".eyJ0YW1wZXJlZCI6dHJ1ZX0." + parts[2]
142    _, err = verifyJWT(tamperedToken, secret)
143    if err != nil {
144        fmt.Printf("✓ Tampering detected: %v\n", err)
145    }
146}
147// run

JWT Security Considerations

Algorithm Selection:

  • HS256 (HMAC with SHA-256): Symmetric signing, fast, good for single-server apps
  • RS256 (RSA with SHA-256): Asymmetric signing, better for microservices where multiple services verify tokens
  • ES256 (ECDSA with SHA-256): Asymmetric, smaller signatures than RSA

Common Attacks:

  1. Algorithm Confusion: Attacker changes algorithm from RS256 to HS256, using public key as HMAC secret

    • Defense: Always specify and validate the expected algorithm
  2. Token Reuse: Stolen tokens used until they expire

    • Defense: Short expiration times and refresh token rotation
  3. Missing Validation: Not checking exp, nbf, iss claims

    • Defense: Always validate all security-relevant claims

Password Hashing

⚠️ Critical Security Rule: NEVER store passwords in plain text or use regular hash functions like SHA-256 for passwords. If your database is compromised, attackers can use rainbow tables to crack common passwords instantly.

Why Special Password Hashing?

Regular hash functions are designed to be fast. This is great for checksums but terrible for passwords. If an attacker gets your password database, they can try billions of password combinations per second with specialized hardware.

Password hashing functions are deliberately slow, making brute-force attacks impractical. They also use salts (random data added to each password) to prevent rainbow table attacks.

Bcrypt for Password Storage

Password hashing is different from regular hashing because it needs to be slow and computationally expensive. Think of it as adding "armor" to your password storage—bcrypt intentionally makes hashing slower to make brute force attacks impractical.

Bcrypt has three key security features:

  1. Built-in salt: Random data added to each password before hashing
  2. Configurable cost factor: Makes the hashing process slower as computers get faster
  3. Adaptive: You can increase the cost over time to keep up with hardware improvements

Real-world Example

When you create an account on any modern website, they use bcrypt or similar algorithms. If their database is breached, attackers get a list of bcrypt hashes, but each one would take years to crack with current technology, giving users time to change their passwords.

 1package main
 2
 3import (
 4    "fmt"
 5    "golang.org/x/crypto/bcrypt"
 6    "time"
 7)
 8
 9func hashPassword(password string) (string, error) {
10    // DefaultCost is 10 (2^10 = 1024 iterations)
11    // Higher cost = more secure but slower
12    hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
13    if err != nil {
14        return "", err
15    }
16    return string(hash), nil
17}
18
19func verifyPassword(password, hash string) bool {
20    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
21    return err == nil
22}
23
24func benchmarkCost(password string, cost int) time.Duration {
25    start := time.Now()
26    bcrypt.GenerateFromPassword([]byte(password), cost)
27    return time.Since(start)
28}
29
30func main() {
31    password := "MySecurePassword123!"
32
33    fmt.Println("=== Password Hashing Demo ===")
34
35    // Hash password
36    hash, err := hashPassword(password)
37    if err != nil {
38        panic(err)
39    }
40    fmt.Printf("Password: %s\n", password)
41    fmt.Printf("Hash: %s\n", hash)
42    fmt.Printf("Hash length: %d characters\n\n", len(hash))
43
44    // Verify correct password
45    if verifyPassword(password, hash) {
46        fmt.Println("✓ Correct password verified")
47    } else {
48        fmt.Println("✗ Correct password rejected")
49    }
50
51    // Verify wrong password
52    if verifyPassword("WrongPassword", hash) {
53        fmt.Println("✗ Wrong password accepted")
54    } else {
55        fmt.Println("✓ Wrong password rejected")
56    }
57
58    // Same password hashes differently each time (due to random salt)
59    fmt.Println("\n=== Salt Demonstration ===")
60    hash2, _ := hashPassword(password)
61    hash3, _ := hashPassword(password)
62    fmt.Printf("Hash 1: %s\n", hash[:29]+"...")
63    fmt.Printf("Hash 2: %s\n", hash2[:29]+"...")
64    fmt.Printf("Hash 3: %s\n", hash3[:29]+"...")
65    fmt.Printf("All different: %v\n", (hash != hash2) && (hash2 != hash3))
66    fmt.Printf("But all verify: %v\n",
67        verifyPassword(password, hash) &&
68        verifyPassword(password, hash2) &&
69        verifyPassword(password, hash3))
70
71    // Benchmark different cost factors
72    fmt.Println("\n=== Cost Factor Impact ===")
73    for cost := 4; cost <= 14; cost += 2 {
74        duration := benchmarkCost(password, cost)
75        fmt.Printf("Cost %2d: %10s (%d iterations)\n", cost, duration, 1<<uint(cost))
76    }
77    fmt.Println("\n⚠️  Higher cost = more secure but slower login")
78    fmt.Println("    Recommended: 10-12 for most applications")
79}
80// run

Password Hashing Algorithms Comparison

bcrypt:

  • ✓ Battle-tested, widely supported
  • ✓ Configurable cost factor
  • ✗ Limited to 72 characters
  • ✗ Less memory-hard than newer alternatives

scrypt:

  • ✓ Memory-hard (resists GPU/ASIC attacks)
  • ✓ Configurable memory, CPU, and parallelization
  • ✗ More complex to configure correctly
  • ✗ Less widely supported

Argon2 (Recommended for new projects):

  • ✓ Winner of Password Hashing Competition
  • ✓ Best resistance against all attack types
  • ✓ Three variants: Argon2d (GPU-resistant), Argon2i (side-channel resistant), Argon2id (hybrid)
  • ✗ Newer, less universally supported

Random Number Generation

Cryptographic security depends on randomness. If random numbers are predictable, encryption becomes breakable. Go provides two sources of randomness:

  • math/rand: Fast pseudorandom numbers. ⚠️ NOT cryptographically secure. Never use for security.
  • crypto/rand: Cryptographically secure random numbers from OS entropy. Always use for keys, nonces, salts.
 1package main
 2
 3import (
 4    "crypto/rand"
 5    "encoding/hex"
 6    "fmt"
 7    "math/big"
 8)
 9
10func generateSecureRandomBytes(n int) ([]byte, error) {
11    b := make([]byte, n)
12    _, err := rand.Read(b)
13    return b, err
14}
15
16func generateSecureRandomInt(max int64) (int64, error) {
17    n, err := rand.Int(rand.Reader, big.NewInt(max))
18    if err != nil {
19        return 0, err
20    }
21    return n.Int64(), nil
22}
23
24func main() {
25    // Generate random bytes
26    randomBytes, _ := generateSecureRandomBytes(32)
27    fmt.Printf("Random bytes (hex): %s\n", hex.EncodeToString(randomBytes))
28
29    // Generate random integers
30    fmt.Println("\nRandom integers (0-99):")
31    for i := 0; i < 5; i++ {
32        n, _ := generateSecureRandomInt(100)
33        fmt.Printf("  %d\n", n)
34    }
35
36    // Generate random token
37    token, _ := generateSecureRandomBytes(32)
38    tokenStr := hex.EncodeToString(token)
39    fmt.Printf("\nSecure token: %s\n", tokenStr)
40    fmt.Printf("Token length: %d characters\n", len(tokenStr))
41}
42// run

Production Best Practices

Building a secure application is like building a fortress—individual components are important, but how they work together determines your overall security. Let's explore some production-ready patterns that bring everything together.

Secure Configuration Management

In production, you can't hardcode encryption keys or passwords in your source code. That's like leaving your house keys under the doormat! Secure configuration management ensures that sensitive information is protected at rest and only accessible to authorized applications.

Common Configuration Pitfalls

  • Hardcoded secrets: Never commit keys, passwords, or certificates to version control
  • Environment variable exposure: Be careful about what environment variables you log
  • Key rotation: Have a strategy to rotate encryption keys regularly
  • Secret management: Use dedicated secret management systems in production (HashiCorp Vault, AWS Secrets Manager, etc.)

Real-world Example

Netflix and AWS use sophisticated key management systems where encryption keys are automatically rotated every 90 days. If a key is compromised, the system can quickly re-encrypt all data with a new key, minimizing the security impact.

  1package main
  2
  3import (
  4    "crypto/aes"
  5    "crypto/cipher"
  6    "crypto/rand"
  7    "encoding/base64"
  8    "fmt"
  9    "io"
 10    "os"
 11)
 12
 13type SecureConfig struct {
 14    encryptionKey []byte
 15}
 16
 17func NewSecureConfig(keyBase64 string) (*SecureConfig, error) {
 18    key, err := base64.StdEncoding.DecodeString(keyBase64)
 19    if err != nil {
 20        return nil, err
 21    }
 22
 23    if len(key) != 32 {
 24        return nil, fmt.Errorf("key must be 32 bytes for AES-256")
 25    }
 26
 27    return &SecureConfig{encryptionKey: key}, nil
 28}
 29
 30func (sc *SecureConfig) EncryptValue(plaintext string) (string, error) {
 31    block, err := aes.NewCipher(sc.encryptionKey)
 32    if err != nil {
 33        return "", err
 34    }
 35
 36    gcm, err := cipher.NewGCM(block)
 37    if err != nil {
 38        return "", err
 39    }
 40
 41    nonce := make([]byte, gcm.NonceSize())
 42    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
 43        return "", err
 44    }
 45
 46    ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
 47    return base64.StdEncoding.EncodeToString(ciphertext), nil
 48}
 49
 50func (sc *SecureConfig) DecryptValue(ciphertext string) (string, error) {
 51    data, err := base64.StdEncoding.DecodeString(ciphertext)
 52    if err != nil {
 53        return "", err
 54    }
 55
 56    block, err := aes.NewCipher(sc.encryptionKey)
 57    if err != nil {
 58        return "", err
 59    }
 60
 61    gcm, err := cipher.NewGCM(block)
 62    if err != nil {
 63        return "", err
 64    }
 65
 66    nonceSize := gcm.NonceSize()
 67    if len(data) < nonceSize {
 68        return "", fmt.Errorf("ciphertext too short")
 69    }
 70
 71    nonce, ciphertext := data[:nonceSize], data[nonceSize:]
 72    plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
 73    if err != nil {
 74        return "", err
 75    }
 76
 77    return string(plaintext), nil
 78}
 79
 80func main() {
 81    fmt.Println("=== Secure Configuration Management ===\n")
 82
 83    // In production, load from environment variable or secret manager
 84    keyBase64 := os.Getenv("ENCRYPTION_KEY")
 85    if keyBase64 == "" {
 86        // Generate a key for demo purposes
 87        key := make([]byte, 32)
 88        rand.Read(key)
 89        keyBase64 = base64.StdEncoding.EncodeToString(key)
 90        fmt.Printf("Generated key (store this securely!):\n%s\n\n", keyBase64)
 91    }
 92
 93    config, err := NewSecureConfig(keyBase64)
 94    if err != nil {
 95        panic(err)
 96    }
 97
 98    // Encrypt sensitive configuration values
 99    secrets := map[string]string{
100        "database_password": "my-database-password-123",
101        "api_key":           "sk-1234567890abcdef",
102        "jwt_secret":        "super-secret-jwt-key",
103    }
104
105    fmt.Println("=== Encrypting Secrets ===")
106    encrypted := make(map[string]string)
107    for name, value := range secrets {
108        enc, err := config.EncryptValue(value)
109        if err != nil {
110            panic(err)
111        }
112        encrypted[name] = enc
113        fmt.Printf("%s: %s...\n", name, enc[:40])
114    }
115
116    // Decrypt when needed
117    fmt.Println("\n=== Decrypting Secrets ===")
118    for name, enc := range encrypted {
119        dec, err := config.DecryptValue(enc)
120        if err != nil {
121            panic(err)
122        }
123        fmt.Printf("%s: %s\n", name, dec)
124        fmt.Printf("  Matches original: %v\n", dec == secrets[name])
125    }
126
127    fmt.Println("\n=== Best Practices ===")
128    fmt.Println("• Store encryption key in environment variable or secret manager")
129    fmt.Println("• Never commit keys to version control")
130    fmt.Println("• Rotate keys regularly (e.g., every 90 days)")
131    fmt.Println("• Use different keys for different environments (dev/staging/prod)")
132    fmt.Println("• Audit key access and usage")
133}
134// run

Defense in Depth

Security isn't about perfect solutions—it's about layers. If one layer fails, others provide protection. Key layers include:

  1. Network Layer: Firewalls, TLS/SSL, VPNs
  2. Application Layer: Input validation, authentication, authorization
  3. Data Layer: Encryption at rest, secure backups
  4. Monitoring Layer: Logging, intrusion detection, alerts

Common Cryptographic Mistakes

Mistake 1: Rolling Your Own Crypto
Never implement your own cryptographic algorithms. Use well-tested libraries. Even small implementation errors can completely break security.

Mistake 2: Using Weak Algorithms
Avoid MD5, SHA1 (for security), DES, RC4. Use SHA-256+, AES, RSA-2048+.

Mistake 3: Reusing Nonces/IVs
Never reuse nonces with the same key in GCM mode. This completely breaks encryption.

Mistake 4: Not Validating Input
Always validate and sanitize user input before cryptographic operations. Prevent injection attacks.

Mistake 5: Ignoring Timing Attacks
Use constant-time comparison functions (hmac.Equal, subtle.ConstantTimeCompare) when comparing secrets.

Mistake 6: Poor Key Management
Hardcoded keys, keys in version control, no key rotation strategy—all recipe for disaster.

Further Reading

Practice Exercises

Exercise 1: Secure File Encryption

Difficulty: Intermediate | Time: 30-40 minutes

Learning Objectives:

  • Master AES-256-GCM authenticated encryption for file security
  • Understand secure file handling and key derivation from passwords
  • Learn to build efficient encryption tools for large files

Real-World Context: File encryption is essential for protecting sensitive data at rest. Whether you're building a secure backup system, protecting configuration files with secrets, or creating a secure file sharing application, AES-256-GCM provides the gold standard for confidentiality and integrity protection.

Build a file encryption/decryption tool with AES-256-GCM that handles large files efficiently. Your tool should derive encryption keys from passwords using secure hashing, handle nonce generation and storage, and provide authenticated encryption that prevents both unauthorized reading and tampering. This exercise demonstrates the fundamental patterns used in production file encryption systems where data confidentiality and integrity are critical requirements.

Solution with Explanation
  1package main
  2
  3import (
  4	"crypto/aes"
  5	"crypto/cipher"
  6	"crypto/rand"
  7	"crypto/sha256"
  8	"fmt"
  9	"io"
 10	"os"
 11)
 12
 13// FileEncryptor handles secure file encryption/decryption
 14type FileEncryptor struct {
 15	key []byte
 16}
 17
 18func NewFileEncryptor(password string) *FileEncryptor {
 19	// Derive key from password using SHA-256
 20	hash := sha256.Sum256([]byte(password))
 21	return &FileEncryptor{key: hash[:]}
 22}
 23
 24func (fe *FileEncryptor) EncryptFile(inputPath, outputPath string) error {
 25	// Read input file
 26	plaintext, err := os.ReadFile(inputPath)
 27	if err != nil {
 28		return fmt.Errorf("failed to read input file: %w", err)
 29	}
 30
 31	// Create cipher
 32	block, err := aes.NewCipher(fe.key)
 33	if err != nil {
 34		return fmt.Errorf("failed to create cipher: %w", err)
 35	}
 36
 37	// Create GCM mode
 38	gcm, err := cipher.NewGCM(block)
 39	if err != nil {
 40		return fmt.Errorf("failed to create GCM: %w", err)
 41	}
 42
 43	// Generate nonce
 44	nonce := make([]byte, gcm.NonceSize())
 45	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
 46		return fmt.Errorf("failed to generate nonce: %w", err)
 47	}
 48
 49	// Encrypt data
 50	ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
 51
 52	// Write encrypted data
 53	if err := os.WriteFile(outputPath, ciphertext, 0600); err != nil {
 54		return fmt.Errorf("failed to write output file: %w", err)
 55	}
 56
 57	fmt.Printf("File encrypted successfully: %s -> %s\n", inputPath, outputPath)
 58	fmt.Printf("Original size: %d bytes, Encrypted size: %d bytes\n", len(plaintext), len(ciphertext))
 59
 60	return nil
 61}
 62
 63func (fe *FileEncryptor) DecryptFile(inputPath, outputPath string) error {
 64	// Read encrypted file
 65	ciphertext, err := os.ReadFile(inputPath)
 66	if err != nil {
 67		return fmt.Errorf("failed to read input file: %w", err)
 68	}
 69
 70	// Create cipher
 71	block, err := aes.NewCipher(fe.key)
 72	if err != nil {
 73		return fmt.Errorf("failed to create cipher: %w", err)
 74	}
 75
 76	// Create GCM mode
 77	gcm, err := cipher.NewGCM(block)
 78	if err != nil {
 79		return fmt.Errorf("failed to create GCM: %w", err)
 80	}
 81
 82	nonceSize := gcm.NonceSize()
 83	if len(ciphertext) < nonceSize {
 84		return fmt.Errorf("ciphertext too short")
 85	}
 86
 87	// Extract nonce and ciphertext
 88	nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
 89
 90	// Decrypt data
 91	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
 92	if err != nil {
 93		return fmt.Errorf("failed to decrypt: %w", err)
 94	}
 95
 96	// Write decrypted data
 97	if err := os.WriteFile(outputPath, plaintext, 0600); err != nil {
 98		return fmt.Errorf("failed to write output file: %w", err)
 99	}
100
101	fmt.Printf("File decrypted successfully: %s -> %s\n", inputPath, outputPath)
102	fmt.Printf("Decrypted size: %d bytes\n", len(plaintext))
103
104	return nil
105}
106
107func main() {
108	password := "my-secure-password-2024"
109	encryptor := NewFileEncryptor(password)
110
111	// Create a test file
112	testData := []byte("This is sensitive data that needs to be encrypted!\nLine 2\nLine 3")
113	if err := os.WriteFile("test.txt", testData, 0600); err != nil {
114		panic(err)
115	}
116
117	// Encrypt file
118	if err := encryptor.EncryptFile("test.txt", "test.txt.enc"); err != nil {
119		fmt.Printf("Encryption error: %v\n", err)
120		return
121	}
122
123	// Decrypt file
124	if err := encryptor.DecryptFile("test.txt.enc", "test_decrypted.txt"); err != nil {
125		fmt.Printf("Decryption error: %v\n", err)
126		return
127	}
128
129	// Verify decryption
130	decrypted, _ := os.ReadFile("test_decrypted.txt")
131	fmt.Printf("\nDecrypted content:\n%s\n", string(decrypted))
132
133	// Cleanup
134	os.Remove("test.txt")
135	os.Remove("test.txt.enc")
136	os.Remove("test_decrypted.txt")
137}
138// run

Explanation:

This secure file encryptor demonstrates:

  • AES-256-GCM: Uses authenticated encryption to prevent tampering
  • Key Derivation: Derives encryption key from password using SHA-256
  • Nonce Handling: Generates random nonce for each encryption
  • File Format: Stores nonce at the beginning of encrypted file
  • Error Handling: Comprehensive error handling for production use
  • Authentication: GCM mode provides both encryption and authentication

Production considerations:

  • Use a proper key derivation function like Argon2 or PBKDF2 instead of plain SHA-256
  • For large files, use streaming encryption to avoid loading entire file into memory
  • Store file metadata securely

Exercise 2: Digital Signature Verification

Difficulty: Intermediate | Time: 25-35 minutes

Learning Objectives:

  • Master RSA digital signatures for authentication and integrity
  • Understand public key cryptography for document verification
  • Learn to build tamper-evident systems with cryptographic proofs

Real-World Context: Digital signatures are the foundation of trust in digital systems. From software distribution to legal document workflows and financial transactions, digital signatures provide mathematical proof that a document came from a specific source and hasn't been altered.

Implement a document signing and verification system using RSA signatures. Your system should generate RSA key pairs, create digital signatures that prove both authenticity and integrity, and include metadata like timestamps and signer information. This exercise demonstrates the core principles behind code signing, document workflows, and blockchain transactions where proving the origin and integrity of digital data is essential for building trustworthy systems.

Solution with Explanation
  1package main
  2
  3import (
  4	"crypto"
  5	"crypto/rand"
  6	"crypto/rsa"
  7	"crypto/sha256"
  8	"crypto/x509"
  9	"encoding/pem"
 10	"fmt"
 11	"os"
 12	"time"
 13)
 14
 15// DocumentSigner handles document signing and verification
 16type DocumentSigner struct {
 17	privateKey *rsa.PrivateKey
 18	publicKey  *rsa.PublicKey
 19}
 20
 21type SignedDocument struct {
 22	Content    []byte
 23	Signature  []byte
 24	SignedAt   time.Time
 25	SignedBy   string
 26}
 27
 28func NewDocumentSigner() (*DocumentSigner, error) {
 29	// Generate RSA key pair
 30	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
 31	if err != nil {
 32		return nil, err
 33	}
 34
 35	return &DocumentSigner{
 36		privateKey: privateKey,
 37		publicKey:  &privateKey.PublicKey,
 38	}, nil
 39}
 40
 41func (ds *DocumentSigner) SignDocument(content []byte, signer string) (*SignedDocument, error) {
 42	// Hash the content
 43	hash := sha256.Sum256(content)
 44
 45	// Sign the hash
 46	signature, err := rsa.SignPKCS1v15(rand.Reader, ds.privateKey, crypto.SHA256, hash[:])
 47	if err != nil {
 48		return nil, fmt.Errorf("failed to sign: %w", err)
 49	}
 50
 51	return &SignedDocument{
 52		Content:   content,
 53		Signature: signature,
 54		SignedAt:  time.Now(),
 55		SignedBy:  signer,
 56	}, nil
 57}
 58
 59func (ds *DocumentSigner) VerifyDocument(doc *SignedDocument) error {
 60	// Hash the content
 61	hash := sha256.Sum256(doc.Content)
 62
 63	// Verify signature
 64	err := rsa.VerifyPKCS1v15(ds.publicKey, crypto.SHA256, hash[:], doc.Signature)
 65	if err != nil {
 66		return fmt.Errorf("signature verification failed: %w", err)
 67	}
 68
 69	return nil
 70}
 71
 72func (ds *DocumentSigner) ExportPublicKey() (string, error) {
 73	pubKeyBytes, err := x509.MarshalPKIXPublicKey(ds.publicKey)
 74	if err != nil {
 75		return "", err
 76	}
 77
 78	pubKeyPEM := pem.EncodeToMemory(&pem.Block{
 79		Type:  "PUBLIC KEY",
 80		Bytes: pubKeyBytes,
 81	})
 82
 83	return string(pubKeyPEM), nil
 84}
 85
 86func (ds *DocumentSigner) ExportPrivateKey() (string, error) {
 87	privKeyBytes := x509.MarshalPKCS1PrivateKey(ds.privateKey)
 88	privKeyPEM := pem.EncodeToMemory(&pem.Block{
 89		Type:  "RSA PRIVATE KEY",
 90		Bytes: privKeyBytes,
 91	})
 92
 93	return string(privKeyPEM), nil
 94}
 95
 96// Save keys to files
 97func (ds *DocumentSigner) SaveKeys(privateKeyPath, publicKeyPath string) error {
 98	// Save private key
 99	privPEM, err := ds.ExportPrivateKey()
100	if err != nil {
101		return err
102	}
103	if err := os.WriteFile(privateKeyPath, []byte(privPEM), 0600); err != nil {
104		return err
105	}
106
107	// Save public key
108	pubPEM, err := ds.ExportPublicKey()
109	if err != nil {
110		return err
111	}
112	if err := os.WriteFile(publicKeyPath, []byte(pubPEM), 0644); err != nil {
113		return err
114	}
115
116	fmt.Printf("Keys saved to %s and %s\n", privateKeyPath, publicKeyPath)
117	return nil
118}
119
120func main() {
121	// Create document signer
122	signer, err := NewDocumentSigner()
123	if err != nil {
124		panic(err)
125	}
126
127	// Document to sign
128	document := []byte("This is an important contract that needs to be signed digitally.")
129	fmt.Printf("Original document:\n%s\n\n", string(document))
130
131	// Sign the document
132	signedDoc, err := signer.SignDocument(document, "Alice")
133	if err != nil {
134		fmt.Printf("Signing error: %v\n", err)
135		return
136	}
137
138	fmt.Printf("Document signed by %s at %s\n", signedDoc.SignedBy, signedDoc.SignedAt.Format(time.RFC3339))
139	fmt.Printf("Signature length: %d bytes\n\n", len(signedDoc.Signature))
140
141	// Verify the signature
142	fmt.Println("=== Verifying Signature ===")
143	if err := signer.VerifyDocument(signedDoc); err != nil {
144		fmt.Printf("Verification failed: %v\n", err)
145	} else {
146		fmt.Println("✓ Signature verified successfully!")
147	}
148
149	// Test with tampered document
150	fmt.Println("\n=== Testing Tampered Document ===")
151	tamperedDoc := &SignedDocument{
152		Content:   []byte("This is a TAMPERED contract!"),
153		Signature: signedDoc.Signature,
154		SignedAt:  signedDoc.SignedAt,
155		SignedBy:  signedDoc.SignedBy,
156	}
157
158	if err := signer.VerifyDocument(tamperedDoc); err != nil {
159		fmt.Printf("✓ Tampering detected: %v\n", err)
160	} else {
161		fmt.Println("✗ Tampered document passed verification")
162	}
163
164	// Export keys
165	fmt.Println("\n=== Exporting Keys ===")
166	pubKey, _ := signer.ExportPublicKey()
167	fmt.Println("Public Key:")
168	fmt.Println(pubKey[:100] + "...")
169}
170// run

Explanation:

This document signing system demonstrates:

  • RSA Signatures: Uses RSA-2048 for digital signatures
  • SHA-256 Hashing: Hashes document before signing
  • Signature Verification: Verifies signatures to detect tampering
  • Metadata: Includes signing timestamp and signer identity
  • Key Export: Can export keys in PEM format
  • Tamper Detection: Demonstrates how modified content fails verification

Production use cases:

  • Code signing for software distribution
  • Document approval workflows
  • API request authentication
  • Blockchain transactions

Exercise 3: Password Manager

Difficulty: Advanced | Time: 35-45 minutes

Learning Objectives:

  • Build secure password storage with proper encryption and key derivation
  • Master bcrypt for secure password hashing and authentication
  • Understand the principles behind secure credential management systems

Real-World Context: Password managers are essential security tools that protect users' most sensitive credentials. They demonstrate the perfect balance between security and usability. Every developer should understand these principles when building any system that handles user credentials.

Build a simple password manager with master password protection and encrypted storage. Your password manager should use bcrypt for secure master password authentication, AES-256-GCM for encrypting stored credentials, and implement CRUD operations for managing password entries. This exercise demonstrates the critical security patterns used in production credential management systems, where protecting passwords from both external attackers and internal breaches is paramount for user security and trust.

Solution with Explanation
  1package main
  2
  3import (
  4	"crypto/aes"
  5	"crypto/cipher"
  6	"crypto/rand"
  7	"crypto/sha256"
  8	"encoding/json"
  9	"fmt"
 10	"io"
 11	"golang.org/x/crypto/bcrypt"
 12)
 13
 14// PasswordManager stores and retrieves encrypted passwords
 15type PasswordManager struct {
 16	masterPasswordHash []byte
 17	encryptionKey      []byte
 18	vault              map[string][]byte // service -> encrypted password
 19}
 20
 21type VaultEntry struct {
 22	Service  string
 23	Username string
 24	Password string
 25	Notes    string
 26}
 27
 28func NewPasswordManager(masterPassword string) (*PasswordManager, error) {
 29	// Hash master password with bcrypt
 30	hash, err := bcrypt.GenerateFromPassword([]byte(masterPassword), bcrypt.DefaultCost)
 31	if err != nil {
 32		return nil, err
 33	}
 34
 35	// Derive encryption key from master password
 36	keyHash := sha256.Sum256([]byte(masterPassword))
 37
 38	return &PasswordManager{
 39		masterPasswordHash: hash,
 40		encryptionKey:      keyHash[:],
 41		vault:              make(map[string][]byte),
 42	}, nil
 43}
 44
 45func (pm *PasswordManager) VerifyMasterPassword(password string) bool {
 46	err := bcrypt.CompareHashAndPassword(pm.masterPasswordHash, []byte(password))
 47	return err == nil
 48}
 49
 50func (pm *PasswordManager) AddPassword(service, username, password, notes string) error {
 51	entry := VaultEntry{
 52		Service:  service,
 53		Username: username,
 54		Password: password,
 55		Notes:    notes,
 56	}
 57
 58	// Convert to JSON
 59	data, err := json.Marshal(entry)
 60	if err != nil {
 61		return err
 62	}
 63
 64	// Encrypt
 65	encrypted, err := pm.encrypt(data)
 66	if err != nil {
 67		return err
 68	}
 69
 70	pm.vault[service] = encrypted
 71	fmt.Printf("✓ Password added for %s\n", service)
 72	return nil
 73}
 74
 75func (pm *PasswordManager) GetPassword(service string) (*VaultEntry, error) {
 76	encrypted, exists := pm.vault[service]
 77	if !exists {
 78		return nil, fmt.Errorf("no password found for service: %s", service)
 79	}
 80
 81	// Decrypt
 82	data, err := pm.decrypt(encrypted)
 83	if err != nil {
 84		return nil, err
 85	}
 86
 87	// Parse JSON
 88	var entry VaultEntry
 89	if err := json.Unmarshal(data, &entry); err != nil {
 90		return nil, err
 91	}
 92
 93	return &entry, nil
 94}
 95
 96func (pm *PasswordManager) ListServices() []string {
 97	services := make([]string, 0, len(pm.vault))
 98	for service := range pm.vault {
 99		services = append(services, service)
100	}
101	return services
102}
103
104func (pm *PasswordManager) DeletePassword(service string) error {
105	if _, exists := pm.vault[service]; !exists {
106		return fmt.Errorf("service not found: %s", service)
107	}
108
109	delete(pm.vault, service)
110	fmt.Printf("✓ Password deleted for %s\n", service)
111	return nil
112}
113
114func (pm *PasswordManager) encrypt(plaintext []byte) ([]byte, error) {
115	block, err := aes.NewCipher(pm.encryptionKey)
116	if err != nil {
117		return nil, err
118	}
119
120	gcm, err := cipher.NewGCM(block)
121	if err != nil {
122		return nil, err
123	}
124
125	nonce := make([]byte, gcm.NonceSize())
126	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
127		return nil, err
128	}
129
130	return gcm.Seal(nonce, nonce, plaintext, nil), nil
131}
132
133func (pm *PasswordManager) decrypt(ciphertext []byte) ([]byte, error) {
134	block, err := aes.NewCipher(pm.encryptionKey)
135	if err != nil {
136		return nil, err
137	}
138
139	gcm, err := cipher.NewGCM(block)
140	if err != nil {
141		return nil, err
142	}
143
144	nonceSize := gcm.NonceSize()
145	if len(ciphertext) < nonceSize {
146		return nil, fmt.Errorf("ciphertext too short")
147	}
148
149	nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
150	return gcm.Open(nil, nonce, ciphertext, nil)
151}
152
153func main() {
154	// Create password manager
155	masterPassword := "MySecureMasterPassword123!"
156	pm, err := NewPasswordManager(masterPassword)
157	if err != nil {
158		panic(err)
159	}
160
161	fmt.Println("=== Password Manager ===\n")
162
163	// Add passwords
164	pm.AddPassword("gmail.com", "john@gmail.com", "SecurePass123!", "Personal email")
165	pm.AddPassword("github.com", "johndoe", "GitH@bP@ss456", "Code repository")
166	pm.AddPassword("aws.com", "john.doe@company.com", "AW$P@ssw0rd!", "Company AWS account")
167
168	// List services
169	fmt.Println("\n=== Stored Services ===")
170	for _, service := range pm.ListServices() {
171		fmt.Printf("- %s\n", service)
172	}
173
174	// Retrieve password
175	fmt.Println("\n=== Retrieving Password ===")
176	entry, err := pm.GetPassword("github.com")
177	if err != nil {
178		fmt.Printf("Error: %v\n", err)
179	} else {
180		fmt.Printf("Service: %s\n", entry.Service)
181		fmt.Printf("Username: %s\n", entry.Username)
182		fmt.Printf("Password: %s\n", entry.Password)
183		fmt.Printf("Notes: %s\n", entry.Notes)
184	}
185
186	// Verify master password
187	fmt.Println("\n=== Master Password Verification ===")
188	if pm.VerifyMasterPassword(masterPassword) {
189		fmt.Println("✓ Master password correct")
190	} else {
191		fmt.Println("✗ Master password incorrect")
192	}
193
194	if pm.VerifyMasterPassword("WrongPassword") {
195		fmt.Println("✗ Wrong password accepted")
196	} else {
197		fmt.Println("✓ Wrong password rejected")
198	}
199
200	// Delete password
201	fmt.Println("\n=== Deleting Password ===")
202	pm.DeletePassword("aws.com")
203
204	fmt.Println("\n=== Final Service List ===")
205	for _, service := range pm.ListServices() {
206		fmt.Printf("- %s\n", service)
207	}
208}
209// run

Explanation:

This password manager demonstrates:

  • Master Password: Uses bcrypt to securely hash the master password
  • AES-GCM Encryption: Encrypts stored passwords with authenticated encryption
  • Key Derivation: Derives encryption key from master password
  • JSON Storage: Stores structured password entries
  • CRUD Operations: Add, retrieve, list, and delete passwords
  • Secure by Default: Uses proper cryptographic primitives

Security considerations:

  • In production, use Argon2 or scrypt instead of SHA-256 for key derivation
  • Store vault data in encrypted file with proper file permissions
  • Implement password generation and strength checking
  • Add time-based unlocking and auto-lock features
  • Consider using hardware security modules for key storage

Exercise 4: TLS Certificate Validator

Difficulty: Advanced | Time: 30-40 minutes

Learning Objectives:

  • Master TLS certificate validation and chain verification
  • Understand certificate security checks and vulnerability detection
  • Learn to build security monitoring tools for PKI infrastructure

Real-World Context: TLS certificates are the backbone of internet security, but misconfigured or expired certificates are one of the most common causes of production outages. Certificate validation tools are essential for proactive security monitoring and preventing service disruptions.

Create a TLS certificate validator that checks certificate validity, chains, and common issues. Your validator should connect to remote servers, analyze certificate chains, detect security weaknesses, and provide comprehensive validation reports. This exercise demonstrates the security monitoring patterns used in production DevOps workflows where automated certificate validation prevents security incidents and service disruptions before they impact users.

Solution with Explanation
  1package main
  2
  3import (
  4	"crypto/tls"
  5	"crypto/x509"
  6	"fmt"
  7	"time"
  8)
  9
 10// CertificateValidator validates TLS certificates
 11type CertificateValidator struct {
 12	rootCAs *x509.CertPool
 13}
 14
 15type ValidationResult struct {
 16	Valid          bool
 17	Errors         []string
 18	Warnings       []string
 19	ExpiresIn      time.Duration
 20	Issuer         string
 21	Subject        string
 22	DNSNames       []string
 23	NotBefore      time.Time
 24	NotAfter       time.Time
 25	SignatureAlgo  string
 26	PublicKeyAlgo  string
 27}
 28
 29func NewCertificateValidator() *CertificateValidator {
 30	// Use system root CAs
 31	rootCAs, _ := x509.SystemCertPool()
 32	if rootCAs == nil {
 33		rootCAs = x509.NewCertPool()
 34	}
 35
 36	return &CertificateValidator{
 37		rootCAs: rootCAs,
 38	}
 39}
 40
 41func (cv *CertificateValidator) ValidateHost(host string, port int) (*ValidationResult, error) {
 42	address := fmt.Sprintf("%s:%d", host, port)
 43
 44	// Connect with TLS
 45	conn, err := tls.Dial("tcp", address, &tls.Config{
 46		InsecureSkipVerify: false,
 47	})
 48	if err != nil {
 49		return nil, fmt.Errorf("TLS connection failed: %w", err)
 50	}
 51	defer conn.Close()
 52
 53	// Get peer certificates
 54	state := conn.ConnectionState()
 55	if len(state.PeerCertificates) == 0 {
 56		return nil, fmt.Errorf("no certificates presented by server")
 57	}
 58
 59	cert := state.PeerCertificates[0]
 60	return cv.ValidateCertificate(cert, host), nil
 61}
 62
 63func (cv *CertificateValidator) ValidateCertificate(cert *x509.Certificate, hostname string) *ValidationResult {
 64	result := &ValidationResult{
 65		Valid:         true,
 66		Errors:        []string{},
 67		Warnings:      []string{},
 68		Issuer:        cert.Issuer.CommonName,
 69		Subject:       cert.Subject.CommonName,
 70		DNSNames:      cert.DNSNames,
 71		NotBefore:     cert.NotBefore,
 72		NotAfter:      cert.NotAfter,
 73		SignatureAlgo: cert.SignatureAlgorithm.String(),
 74		PublicKeyAlgo: cert.PublicKeyAlgorithm.String(),
 75	}
 76
 77	// Calculate expiry
 78	result.ExpiresIn = time.Until(cert.NotAfter)
 79
 80	// Check if expired
 81	now := time.Now()
 82	if now.After(cert.NotAfter) {
 83		result.Valid = false
 84		result.Errors = append(result.Errors, "Certificate has expired")
 85	}
 86
 87	// Check if not yet valid
 88	if now.Before(cert.NotBefore) {
 89		result.Valid = false
 90		result.Errors = append(result.Errors, "Certificate is not yet valid")
 91	}
 92
 93	// Warn if expiring soon
 94	if result.ExpiresIn > 0 && result.ExpiresIn < 30*24*time.Hour {
 95		result.Warnings = append(result.Warnings,
 96			fmt.Sprintf("Certificate expires in %d days", int(result.ExpiresIn.Hours()/24)))
 97	}
 98
 99	// Verify hostname
100	if hostname != "" {
101		if err := cert.VerifyHostname(hostname); err != nil {
102			result.Valid = false
103			result.Errors = append(result.Errors, fmt.Sprintf("Hostname verification failed: %v", err))
104		}
105	}
106
107	// Check key size
108	switch pub := cert.PublicKey.(type) {
109	case interface{ Size() int }:
110		keySize := pub.Size() * 8
111		if keySize < 2048 {
112			result.Warnings = append(result.Warnings,
113				fmt.Sprintf("Weak key size: %d bits", keySize))
114		}
115	}
116
117	// Check signature algorithm
118	weakAlgorithms := map[x509.SignatureAlgorithm]bool{
119		x509.MD2WithRSA:    true,
120		x509.MD5WithRSA:    true,
121		x509.SHA1WithRSA:   true,
122		x509.DSAWithSHA1:   true,
123		x509.ECDSAWithSHA1: true,
124	}
125
126	if weakAlgorithms[cert.SignatureAlgorithm] {
127		result.Warnings = append(result.Warnings,
128			fmt.Sprintf("Weak signature algorithm: %s", cert.SignatureAlgorithm))
129	}
130
131	// Verify certificate chain
132	opts := x509.VerifyOptions{
133		Roots:         cv.rootCAs,
134		CurrentTime:   now,
135		DNSName:       hostname,
136		Intermediates: x509.NewCertPool(),
137	}
138
139	if _, err := cert.Verify(opts); err != nil {
140		result.Valid = false
141		result.Errors = append(result.Errors, fmt.Sprintf("Chain verification failed: %v", err))
142	}
143
144	return result
145}
146
147func (result *ValidationResult) Print() {
148	fmt.Println("=== Certificate Validation Result ===")
149	fmt.Printf("Subject: %s\n", result.Subject)
150	fmt.Printf("Issuer: %s\n", result.Issuer)
151	fmt.Printf("Valid: %v\n", result.Valid)
152
153	if len(result.DNSNames) > 0 {
154		fmt.Printf("DNS Names: %v\n", result.DNSNames)
155	}
156
157	fmt.Printf("Not Before: %s\n", result.NotBefore.Format(time.RFC3339))
158	fmt.Printf("Not After: %s\n", result.NotAfter.Format(time.RFC3339))
159
160	if result.ExpiresIn > 0 {
161		days := int(result.ExpiresIn.Hours() / 24)
162		fmt.Printf("Expires in: %d days\n", days)
163	}
164
165	fmt.Printf("Signature Algorithm: %s\n", result.SignatureAlgo)
166	fmt.Printf("Public Key Algorithm: %s\n", result.PublicKeyAlgo)
167
168	if len(result.Errors) > 0 {
169		fmt.Println("\nErrors:")
170		for _, err := range result.Errors {
171			fmt.Printf("  ✗ %s\n", err)
172		}
173	}
174
175	if len(result.Warnings) > 0 {
176		fmt.Println("\nWarnings:")
177		for _, warn := range result.Warnings {
178			fmt.Printf("  ⚠ %s\n", warn)
179		}
180	}
181
182	if result.Valid && len(result.Warnings) == 0 {
183		fmt.Println("\n✓ Certificate is valid and secure")
184	}
185}
186
187func main() {
188	validator := NewCertificateValidator()
189
190	// Test with real websites
191	testHosts := []struct {
192		host string
193		port int
194	}{
195		{"golang.org", 443},
196		{"example.com", 443},
197	}
198
199	for _, test := range testHosts {
200		fmt.Printf("\n=== Validating %s ===\n", test.host)
201		result, err := validator.ValidateHost(test.host, test.port)
202		if err != nil {
203			fmt.Printf("Validation error: %v\n", err)
204			continue
205		}
206
207		result.Print()
208	}
209}
210// run

Explanation:

This certificate validator demonstrates:

  • TLS Connection: Connects to servers and retrieves certificates
  • Expiry Checking: Detects expired and soon-to-expire certificates
  • Hostname Verification: Validates certificate matches hostname
  • Chain Validation: Verifies complete certificate chain
  • Security Checks: Detects weak algorithms and key sizes
  • Comprehensive Reporting: Provides detailed validation results

Production use cases:

  • Monitoring SSL certificate expiry
  • Validating internal CA certificates
  • Security auditing of TLS configurations
  • Automated certificate renewal alerts

Exercise 5: Token-Based Authentication

Difficulty: Advanced | Time: 40-50 minutes

Learning Objectives:

  • Build secure JWT-like token systems with HMAC signatures
  • Master refresh token patterns and token rotation strategies
  • Understand stateless authentication and session management

Real-World Context: Token-based authentication is the standard for modern web and mobile applications. It enables scalable, stateless authentication that works seamlessly across microservices, load balancers, and distributed systems. Companies like Google, GitHub, and AWS use similar token systems for billions of authentication requests daily.

Implement a secure token-based authentication system with refresh tokens and expiry. Your system should create signed access tokens with user claims, implement secure refresh token rotation, handle token expiry gracefully, and provide comprehensive security features to prevent common attacks like token replay and reuse. This exercise demonstrates the authentication patterns used in production identity systems where balancing security, performance, and usability is critical for protecting user accounts while providing seamless experiences.

Solution with Explanation
  1package main
  2
  3import (
  4	"crypto/hmac"
  5	"crypto/rand"
  6	"crypto/sha256"
  7	"encoding/base64"
  8	"encoding/json"
  9	"fmt"
 10	"strings"
 11	"sync"
 12	"time"
 13)
 14
 15// TokenManager handles token creation and validation
 16type TokenManager struct {
 17	secret         []byte
 18	accessTTL      time.Duration
 19	refreshTTL     time.Duration
 20	refreshTokens  map[string]*RefreshTokenData
 21	mu             sync.RWMutex
 22}
 23
 24type TokenPair struct {
 25	AccessToken  string
 26	RefreshToken string
 27	ExpiresIn    int64 // seconds
 28}
 29
 30type AccessTokenClaims struct {
 31	UserID    int64  `json:"user_id"`
 32	Username  string `json:"username"`
 33	Role      string `json:"role"`
 34	IssuedAt  int64  `json:"iat"`
 35	ExpiresAt int64  `json:"exp"`
 36}
 37
 38type RefreshTokenData struct {
 39	UserID    int64
 40	CreatedAt time.Time
 41	ExpiresAt time.Time
 42	Used      bool
 43}
 44
 45func NewTokenManager(secret string, accessTTL, refreshTTL time.Duration) *TokenManager {
 46	return &TokenManager{
 47		secret:        []byte(secret),
 48		accessTTL:     accessTTL,
 49		refreshTTL:    refreshTTL,
 50		refreshTokens: make(map[string]*RefreshTokenData),
 51	}
 52}
 53
 54func (tm *TokenManager) GenerateTokenPair(userID int64, username, role string) (*TokenPair, error) {
 55	// Create access token
 56	now := time.Now()
 57	accessClaims := AccessTokenClaims{
 58		UserID:    userID,
 59		Username:  username,
 60		Role:      role,
 61		IssuedAt:  now.Unix(),
 62		ExpiresAt: now.Add(tm.accessTTL).Unix(),
 63	}
 64
 65	accessToken, err := tm.createToken(accessClaims)
 66	if err != nil {
 67		return nil, err
 68	}
 69
 70	// Create refresh token
 71	refreshToken, err := tm.createRefreshToken(userID)
 72	if err != nil {
 73		return nil, err
 74	}
 75
 76	return &TokenPair{
 77		AccessToken:  accessToken,
 78		RefreshToken: refreshToken,
 79		ExpiresIn:    int64(tm.accessTTL.Seconds()),
 80	}, nil
 81}
 82
 83func (tm *TokenManager) createToken(claims AccessTokenClaims) (string, error) {
 84	// Create JWT-like token
 85	header := map[string]string{
 86		"alg": "HS256",
 87		"typ": "JWT",
 88	}
 89
 90	headerJSON, _ := json.Marshal(header)
 91	claimsJSON, _ := json.Marshal(claims)
 92
 93	headerEncoded := base64.RawURLEncoding.EncodeToString(headerJSON)
 94	claimsEncoded := base64.RawURLEncoding.EncodeToString(claimsJSON)
 95
 96	message := headerEncoded + "." + claimsEncoded
 97
 98	// Create signature
 99	h := hmac.New(sha256.New, tm.secret)
100	h.Write([]byte(message))
101	signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
102
103	return message + "." + signature, nil
104}
105
106func (tm *TokenManager) createRefreshToken(userID int64) (string, error) {
107	// Generate random token
108	tokenBytes := make([]byte, 32)
109	if _, err := rand.Read(tokenBytes); err != nil {
110		return "", err
111	}
112
113	token := base64.RawURLEncoding.EncodeToString(tokenBytes)
114
115	// Store token data
116	tm.mu.Lock()
117	tm.refreshTokens[token] = &RefreshTokenData{
118		UserID:    userID,
119		CreatedAt: time.Now(),
120		ExpiresAt: time.Now().Add(tm.refreshTTL),
121		Used:      false,
122	}
123	tm.mu.Unlock()
124
125	return token, nil
126}
127
128func (tm *TokenManager) ValidateAccessToken(token string) (*AccessTokenClaims, error) {
129	parts := strings.Split(token, ".")
130	if len(parts) != 3 {
131		return nil, fmt.Errorf("invalid token format")
132	}
133
134	// Verify signature
135	message := parts[0] + "." + parts[1]
136	h := hmac.New(sha256.New, tm.secret)
137	h.Write([]byte(message))
138	expectedSig := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
139
140	if !hmac.Equal([]byte(expectedSig), []byte(parts[2])) {
141		return nil, fmt.Errorf("invalid signature")
142	}
143
144	// Decode claims
145	claimsJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
146	if err != nil {
147		return nil, fmt.Errorf("invalid claims encoding")
148	}
149
150	var claims AccessTokenClaims
151	if err := json.Unmarshal(claimsJSON, &claims); err != nil {
152		return nil, fmt.Errorf("invalid claims format")
153	}
154
155	// Check expiry
156	if time.Now().Unix() > claims.ExpiresAt {
157		return nil, fmt.Errorf("token expired")
158	}
159
160	return &claims, nil
161}
162
163func (tm *TokenManager) RefreshAccessToken(refreshToken string) (*TokenPair, error) {
164	tm.mu.Lock()
165	defer tm.mu.Unlock()
166
167	tokenData, exists := tm.refreshTokens[refreshToken]
168	if !exists {
169		return nil, fmt.Errorf("invalid refresh token")
170	}
171
172	if tokenData.Used {
173		return nil, fmt.Errorf("refresh token already used")
174	}
175
176	if time.Now().After(tokenData.ExpiresAt) {
177		return nil, fmt.Errorf("refresh token expired")
178	}
179
180	// Mark as used
181	tokenData.Used = true
182
183	// Generate new token pair
184	// Note: In production, you'd fetch user details from database
185	return tm.GenerateTokenPair(tokenData.UserID, "user", "user")
186}
187
188func (tm *TokenManager) RevokeRefreshToken(refreshToken string) error {
189	tm.mu.Lock()
190	defer tm.mu.Unlock()
191
192	delete(tm.refreshTokens, refreshToken)
193	return nil
194}
195
196// Cleanup expired refresh tokens
197func (tm *TokenManager) CleanupExpiredTokens() {
198	tm.mu.Lock()
199	defer tm.mu.Unlock()
200
201	now := time.Now()
202	for token, data := range tm.refreshTokens {
203		if now.After(data.ExpiresAt) {
204			delete(tm.refreshTokens, token)
205		}
206	}
207}
208
209func main() {
210	// Create token manager
211	secret := "my-secret-key-change-in-production"
212	tm := NewTokenManager(secret, 15*time.Minute, 7*24*time.Hour)
213
214	fmt.Println("=== Token-Based Authentication System ===\n")
215
216	// 1. Generate initial token pair
217	fmt.Println("1. Generating initial token pair...")
218	tokenPair, err := tm.GenerateTokenPair(12345, "john_doe", "admin")
219	if err != nil {
220		panic(err)
221	}
222
223	fmt.Printf("Access Token: %s...\n", tokenPair.AccessToken[:50])
224	fmt.Printf("Refresh Token: %s...\n", tokenPair.RefreshToken[:30])
225	fmt.Printf("Expires in: %d seconds\n", tokenPair.ExpiresIn)
226
227	// 2. Validate access token
228	fmt.Println("\n2. Validating access token...")
229	claims, err := tm.ValidateAccessToken(tokenPair.AccessToken)
230	if err != nil {
231		fmt.Printf("Validation failed: %v\n", err)
232	} else {
233		fmt.Printf("✓ Token valid\n")
234		fmt.Printf("  User ID: %d\n", claims.UserID)
235		fmt.Printf("  Username: %s\n", claims.Username)
236		fmt.Printf("  Role: %s\n", claims.Role)
237		fmt.Printf("  Issued At: %s\n", time.Unix(claims.IssuedAt, 0).Format(time.RFC3339))
238		fmt.Printf("  Expires At: %s\n", time.Unix(claims.ExpiresAt, 0).Format(time.RFC3339))
239	}
240
241	// 3. Try to validate tampered token
242	fmt.Println("\n3. Testing tampered token...")
243	tamperedToken := tokenPair.AccessToken[:len(tokenPair.AccessToken)-10] + "XXXXXXXXXX"
244	_, err = tm.ValidateAccessToken(tamperedToken)
245	if err != nil {
246		fmt.Printf("✓ Tampered token rejected: %v\n", err)
247	}
248
249	// 4. Refresh tokens
250	fmt.Println("\n4. Refreshing access token...")
251	newTokenPair, err := tm.RefreshAccessToken(tokenPair.RefreshToken)
252	if err != nil {
253		fmt.Printf("Refresh failed: %v\n", err)
254	} else {
255		fmt.Printf("✓ New access token generated: %s...\n", newTokenPair.AccessToken[:50])
256		fmt.Printf("✓ New refresh token generated: %s...\n", newTokenPair.RefreshToken[:30])
257	}
258
259	// 5. Try to reuse refresh token
260	fmt.Println("\n5. Testing refresh token reuse...")
261	_, err = tm.RefreshAccessToken(tokenPair.RefreshToken)
262	if err != nil {
263		fmt.Printf("✓ Refresh token reuse prevented: %v\n", err)
264	}
265
266	// 6. Cleanup
267	fmt.Println("\n6. Cleaning up expired tokens...")
268	tm.CleanupExpiredTokens()
269	fmt.Println("✓ Cleanup completed")
270}
271// run

Explanation:

This authentication system demonstrates:

  • JWT-like Tokens: Creates signed access tokens with claims
  • Refresh Tokens: Long-lived tokens for obtaining new access tokens
  • HMAC Signing: Uses HMAC-SHA256 for token integrity
  • Expiry Management: Separate TTLs for access and refresh tokens
  • One-Time Use: Refresh tokens can only be used once
  • Token Rotation: New refresh token issued on each refresh
  • Cleanup: Removes expired refresh tokens

Security features:

  • Signature verification prevents tampering
  • Short-lived access tokens limit exposure
  • Refresh token rotation prevents replay attacks
  • One-time use refresh tokens enhance security

Production improvements:

  • Store refresh tokens in database, not memory
  • Add jti for token revocation
  • Implement token blacklisting
  • Add rate limiting for token refresh
  • Use RS256 instead of HS256 for distributed systems

Summary

Cryptography in Go provides the building blocks for creating secure applications, but knowing which tool to use is crucial for both security and performance.

Key Cryptographic Primitives:

  • Hashing: Use SHA-256 or SHA-512 for data integrity verification
  • Encryption: Use AES-GCM for symmetric encryption
  • Key Exchange: Use RSA 2048+ or ECDSA for asymmetric encryption
  • Password Storage: Always use bcrypt/scrypt/argon2 for passwords
  • Authentication: Use HMAC for message authentication codes

Security Best Practices:

  • Never implement your own crypto algorithms—use Go's standard library
  • Use cryptographically secure random numbers from crypto/rand
  • Store encryption keys securely in HSMs or secret management systems
  • Rotate keys regularly—have a plan for when keys are compromised
  • Always use TLS for network communication—never send sensitive data over plain HTTP
  • Validate all inputs—prevent injection attacks that could bypass crypto
  • Handle errors securely—don't leak information through error messages
  • Use authenticated encryption (like GCM) to provide both confidentiality and integrity

Common Pitfalls to Avoid:

  • Reusing nonces in GCM mode
  • Using weak hash functions (MD5, SHA1) for security
  • Storing passwords with simple hashing instead of bcrypt
  • Hardcoding secrets in source code
  • Not validating certificate chains
  • Using ECB mode for encryption
  • Ignoring timing attacks in security-critical comparisons

💡 Final Key Takeaway: Cryptography is a powerful tool, but it's only one part of a comprehensive security strategy. The best crypto implementation can be undone by a simple vulnerability elsewhere in your system. Think in layers—defense in depth is your strongest approach. Security is not a feature you add at the end; it's a foundation you build from the start.