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:
-
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.
-
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.
-
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).
Key Management: The Weakest Link
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:
- Deterministic: The same input always produces the same hash output
- Quick Computation: Fast to compute the hash for any input
- Avalanche Effect: Changing a single bit in the input completely changes the output
- One-way: Computationally infeasible to reverse the hash to get the original input
- 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:
- Prevents tampering: An attacker can't modify the ciphertext without detection
- Prevents bit-flipping attacks: Changing any bit causes decryption to fail
- Efficient: Uses parallel operations for high performance
- Standard: Widely used in TLS 1.3, IPsec, and other protocols
Encryption Best Practices
-
Never reuse nonces: Each encryption operation must use a unique nonce. Nonce reuse with GCM completely breaks security.
-
Store nonces with ciphertext: The nonce doesn't need to be secret, so prepending it to the ciphertext is standard practice.
-
Use authenticated encryption: Always use modes like GCM that provide both encryption and authentication. Never use encryption without integrity verification.
-
Generate keys properly: Use
crypto/randfor all key generation. Never derive keys from predictable sources. -
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:
- Authentication: The message came from the private key holder
- Integrity: The message hasn't been tampered with
- 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:
- Hash the message to create a fixed-size digest
- Encrypt the hash with your private key to create the signature
- Recipient hashes the message and decrypts signature with your public key
- 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:
- Root CAs: A small set of trusted certificate authorities built into your operating system and browsers
- Intermediate CAs: Certificate authorities trusted by root CAs, creating a chain
- 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
-
Automate renewal: Certificates expire. Use tools like certbot for Let's Encrypt automation.
-
Use short validity periods: Modern certificates use 90-day validity. This limits the damage from compromised keys.
-
Monitor expiration: Set up alerts at least 30 days before expiration.
-
Include SANs: Use Subject Alternative Names to support multiple domains with one certificate.
-
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
-
Header: Metadata about the token (algorithm, type)
1{"alg": "HS256", "typ": "JWT"} -
Payload: The actual data/claims (user ID, permissions, expiry)
1{"user_id": 123, "exp": 1234567890} -
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:
-
Algorithm Confusion: Attacker changes algorithm from RS256 to HS256, using public key as HMAC secret
- Defense: Always specify and validate the expected algorithm
-
Token Reuse: Stolen tokens used until they expire
- Defense: Short expiration times and refresh token rotation
-
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:
- Built-in salt: Random data added to each password before hashing
- Configurable cost factor: Makes the hashing process slower as computers get faster
- 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:
- Network Layer: Firewalls, TLS/SSL, VPNs
- Application Layer: Input validation, authentication, authorization
- Data Layer: Encryption at rest, secure backups
- 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
- Go crypto package
- Crypto Best Practices
- OWASP Cryptographic Storage Cheat Sheet
- NCC Group Cryptography Services
- Cryptopals Crypto Challenges
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.