main
Raw Download raw file
  1// Package certauth provides certificate authority functionality for HTTPS MITM proxying.
  2//
  3// The CertificateAuthority generates and caches TLS certificates for intercepted
  4// HTTPS connections. When a client makes an HTTPS request through the proxy,
  5// a certificate is dynamically generated (or retrieved from cache) for the
  6// target hostname, allowing the proxy to decrypt, record, and re-encrypt traffic.
  7//
  8// Certificates are stored in an in-memory cache (using sync.Map for thread-safe
  9// access) and optionally persisted to disk for debugging or reuse across restarts.
 10// The CA certificate itself is either loaded from disk or generated on first run.
 11//
 12// Thread-safety:
 13//   - Certificate generation uses a mutex with double-check locking to prevent
 14//     multiple goroutines from generating certificates for the same hostname.
 15//   - The cert cache uses sync.Map for lock-free reads in the common case.
 16//   - Certificate files are written with 0600 permissions for security.
 17package certauth
 18
 19import (
 20	"crypto/rand"
 21	"crypto/rsa"
 22	"crypto/x509"
 23	"crypto/x509/pkix"
 24	"encoding/pem"
 25	"fmt"
 26	"log/slog"
 27	"math/big"
 28	"os"
 29	"path/filepath"
 30	"sync"
 31	"time"
 32)
 33
 34// CertificateAuthority manages CA and host certificate generation
 35type CertificateAuthority struct {
 36	caFile    string
 37	certsDir  string
 38	caName    string
 39	caCert    *x509.Certificate
 40	caKey     *rsa.PrivateKey
 41	certCache sync.Map // hostname -> *tls.Certificate
 42	mu        sync.Mutex
 43	logger    *slog.Logger
 44}
 45
 46// NewCertificateAuthority creates or loads a certificate authority
 47func NewCertificateAuthority(caFile, certsDir, caName string, logger *slog.Logger) (*CertificateAuthority, error) {
 48	if logger == nil {
 49		logger = slog.Default()
 50	}
 51
 52	ca := &CertificateAuthority{
 53		caFile:   caFile,
 54		certsDir: certsDir,
 55		caName:   caName,
 56		logger:   logger,
 57	}
 58
 59	// Create certs directory if it doesn't exist
 60	if err := os.MkdirAll(certsDir, 0755); err != nil {
 61		return nil, fmt.Errorf("failed to create certs directory: %w", err)
 62	}
 63
 64	// Load or create CA certificate
 65	if err := ca.loadOrCreateCA(); err != nil {
 66		return nil, fmt.Errorf("failed to initialize CA: %w", err)
 67	}
 68
 69	return ca, nil
 70}
 71
 72// loadOrCreateCA loads an existing CA or creates a new one
 73func (ca *CertificateAuthority) loadOrCreateCA() error {
 74	// Check if CA file exists
 75	if _, err := os.Stat(ca.caFile); err == nil {
 76		// Load existing CA
 77		return ca.loadCA()
 78	}
 79
 80	// Create new CA
 81	return ca.createCA()
 82}
 83
 84// loadCA loads an existing CA certificate and key
 85func (ca *CertificateAuthority) loadCA() error {
 86	ca.logger.Info("loading CA certificate", "file", ca.caFile)
 87
 88	// Read CA file
 89	caData, err := os.ReadFile(ca.caFile)
 90	if err != nil {
 91		return fmt.Errorf("failed to read CA file: %w", err)
 92	}
 93
 94	// Parse PEM blocks
 95	var certPEM, keyPEM *pem.Block
 96	remaining := caData
 97
 98	for {
 99		block, rest := pem.Decode(remaining)
100		if block == nil {
101			break
102		}
103
104		switch block.Type {
105		case "CERTIFICATE":
106			certPEM = block
107		case "RSA PRIVATE KEY":
108			keyPEM = block
109		}
110
111		remaining = rest
112	}
113
114	if certPEM == nil || keyPEM == nil {
115		return fmt.Errorf("CA file must contain both certificate and private key")
116	}
117
118	// Parse certificate
119	cert, err := x509.ParseCertificate(certPEM.Bytes)
120	if err != nil {
121		return fmt.Errorf("failed to parse CA certificate: %w", err)
122	}
123	ca.caCert = cert
124
125	// Parse private key
126	key, err := x509.ParsePKCS1PrivateKey(keyPEM.Bytes)
127	if err != nil {
128		return fmt.Errorf("failed to parse CA private key: %w", err)
129	}
130	ca.caKey = key
131
132	ca.logger.Info("loaded CA certificate",
133		"subject", cert.Subject.CommonName,
134		"valid_from", cert.NotBefore,
135		"valid_to", cert.NotAfter)
136
137	return nil
138}
139
140// createCA creates a new CA certificate and key
141func (ca *CertificateAuthority) createCA() error {
142	ca.logger.Info("creating new CA certificate", "file", ca.caFile)
143
144	// Generate private key
145	key, err := rsa.GenerateKey(rand.Reader, 2048)
146	if err != nil {
147		return fmt.Errorf("failed to generate CA private key: %w", err)
148	}
149	ca.caKey = key
150
151	// Create CA certificate template
152	serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
153	if err != nil {
154		return fmt.Errorf("failed to generate serial number: %w", err)
155	}
156
157	template := &x509.Certificate{
158		SerialNumber: serialNumber,
159		Subject: pkix.Name{
160			CommonName:   ca.caName,
161			Organization: []string{"gowarcprox"},
162		},
163		NotBefore:             time.Now().Add(-24 * time.Hour), // Valid from 1 day ago
164		NotAfter:              time.Now().Add(3 * 365 * 24 * time.Hour), // 3 years
165		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
166		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
167		BasicConstraintsValid: true,
168		IsCA:                  true,
169		MaxPathLen:            0,
170	}
171
172	// Self-sign the certificate
173	certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
174	if err != nil {
175		return fmt.Errorf("failed to create CA certificate: %w", err)
176	}
177
178	// Parse the certificate
179	cert, err := x509.ParseCertificate(certDER)
180	if err != nil {
181		return fmt.Errorf("failed to parse created certificate: %w", err)
182	}
183	ca.caCert = cert
184
185	// Save to file with restricted permissions (0600 - owner read/write only)
186	// because the file contains a private key
187	f, err := os.OpenFile(ca.caFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
188	if err != nil {
189		return fmt.Errorf("failed to create CA file: %w", err)
190	}
191	defer f.Close()
192
193	// Write certificate
194	if err := pem.Encode(f, &pem.Block{
195		Type:  "CERTIFICATE",
196		Bytes: certDER,
197	}); err != nil {
198		return fmt.Errorf("failed to write CA certificate: %w", err)
199	}
200
201	// Write private key
202	if err := pem.Encode(f, &pem.Block{
203		Type:  "RSA PRIVATE KEY",
204		Bytes: x509.MarshalPKCS1PrivateKey(key),
205	}); err != nil {
206		return fmt.Errorf("failed to write CA private key: %w", err)
207	}
208
209	ca.logger.Info("created new CA certificate",
210		"subject", cert.Subject.CommonName,
211		"valid_from", cert.NotBefore,
212		"valid_to", cert.NotAfter)
213
214	return nil
215}
216
217// GetCertificate returns a certificate for the given hostname
218// Generates a new one if not in cache
219func (ca *CertificateAuthority) GetCertificate(hostname string) ([]byte, []byte, error) {
220	// Check cache first
221	if cached, ok := ca.certCache.Load(hostname); ok {
222		certData := cached.(certKeyPair)
223		return certData.cert, certData.key, nil
224	}
225
226	// Generate new certificate
227	return ca.generateHostCert(hostname)
228}
229
230type certKeyPair struct {
231	cert []byte
232	key  []byte
233}
234
235// generateHostCert generates a new certificate for the hostname
236func (ca *CertificateAuthority) generateHostCert(hostname string) ([]byte, []byte, error) {
237	ca.mu.Lock()
238	defer ca.mu.Unlock()
239
240	// Double-check cache after acquiring lock
241	if cached, ok := ca.certCache.Load(hostname); ok {
242		certData := cached.(certKeyPair)
243		return certData.cert, certData.key, nil
244	}
245
246	ca.logger.Debug("generating certificate for hostname", "hostname", hostname)
247
248	// Generate private key for host
249	key, err := rsa.GenerateKey(rand.Reader, 2048)
250	if err != nil {
251		return nil, nil, fmt.Errorf("failed to generate host private key: %w", err)
252	}
253
254	// Generate serial number
255	serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
256	if err != nil {
257		return nil, nil, fmt.Errorf("failed to generate serial number: %w", err)
258	}
259
260	// Create certificate template
261	template := &x509.Certificate{
262		SerialNumber: serialNumber,
263		Subject: pkix.Name{
264			CommonName:   hostname,
265			Organization: []string{"gowarcprox"},
266		},
267		NotBefore:             time.Now().Add(-24 * time.Hour),
268		NotAfter:              time.Now().Add(3 * 365 * 24 * time.Hour), // 3 years
269		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
270		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
271		BasicConstraintsValid: true,
272		DNSNames:              []string{hostname},
273	}
274
275	// Support wildcard certificates
276	if len(hostname) > 0 && hostname[0] != '*' {
277		template.DNSNames = append(template.DNSNames, "*."+hostname)
278	}
279
280	// Sign with CA
281	certDER, err := x509.CreateCertificate(rand.Reader, template, ca.caCert, &key.PublicKey, ca.caKey)
282	if err != nil {
283		return nil, nil, fmt.Errorf("failed to create host certificate: %w", err)
284	}
285
286	// Encode certificate to PEM
287	certPEM := pem.EncodeToMemory(&pem.Block{
288		Type:  "CERTIFICATE",
289		Bytes: certDER,
290	})
291
292	// Encode private key to PEM
293	keyPEM := pem.EncodeToMemory(&pem.Block{
294		Type:  "RSA PRIVATE KEY",
295		Bytes: x509.MarshalPKCS1PrivateKey(key),
296	})
297
298	// Save to cache
299	ca.certCache.Store(hostname, certKeyPair{cert: certPEM, key: keyPEM})
300
301	// Optionally save to disk
302	if err := ca.saveCertToDisk(hostname, certPEM, keyPEM); err != nil {
303		ca.logger.Warn("failed to save certificate to disk",
304			"hostname", hostname,
305			"error", err)
306		// Don't return error - caching in memory is sufficient
307	}
308
309	ca.logger.Info("generated certificate for hostname", "hostname", hostname)
310	return certPEM, keyPEM, nil
311}
312
313// saveCertToDisk saves a certificate and key to disk
314func (ca *CertificateAuthority) saveCertToDisk(hostname string, certPEM, keyPEM []byte) error {
315	// Sanitize hostname for filename
316	filename := filepath.Join(ca.certsDir, hostname+".pem")
317
318	// Use restricted permissions (0600 - owner read/write only)
319	// because the file contains a private key
320	f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
321	if err != nil {
322		return fmt.Errorf("failed to create cert file: %w", err)
323	}
324	defer f.Close()
325
326	if _, err := f.Write(certPEM); err != nil {
327		return fmt.Errorf("failed to write certificate: %w", err)
328	}
329
330	if _, err := f.Write(keyPEM); err != nil {
331		return fmt.Errorf("failed to write private key: %w", err)
332	}
333
334	return nil
335}
336
337// GetCACert returns the CA certificate in PEM format
338func (ca *CertificateAuthority) GetCACert() []byte {
339	return pem.EncodeToMemory(&pem.Block{
340		Type:  "CERTIFICATE",
341		Bytes: ca.caCert.Raw,
342	})
343}