main
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}