internal/tls/cert.go

package tls

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"fmt"
	"log/slog"
	"math/big"
	"net"
	"time"
)

const (
	CertificateValidityDuration = 365 * 24 * time.Hour // 1 year
)

// GetLocalIPAddresses returns all local IP addresses for certificate inclusion.
func GetLocalIPAddresses(listenAddr string) ([]net.IP, error) {
	// Get all local IP addresses
	ipAddresses := []net.IP{
		net.IPv4(127, 0, 0, 1),
		net.IPv6loopback,
	}

	// Add the listen address if it's a specific IP
	if listenAddr != "" && listenAddr != "0.0.0.0" && listenAddr != "::" {
		if ip := net.ParseIP(listenAddr); ip != nil {
			ipAddresses = append(ipAddresses, ip)
		}
	}

	// If listening on all interfaces (0.0.0.0), get all local IPs
	if listenAddr == "" || listenAddr == "0.0.0.0" || listenAddr == "::" {
		ifaces, err := net.Interfaces()
		if err == nil {
			for _, iface := range ifaces {
				addrs, err := iface.Addrs()
				if err != nil {
					continue
				}
				for _, addr := range addrs {
					var ip net.IP
					switch v := addr.(type) {
					case *net.IPNet:
						ip = v.IP
					case *net.IPAddr:
						ip = v.IP
					}
					if ip != nil && !ip.IsLoopback() {
						// Check if we already have this IP
						exists := false
						for _, existingIP := range ipAddresses {
							if existingIP.Equal(ip) {
								exists = true
								break
							}
						}
						if !exists {
							ipAddresses = append(ipAddresses, ip)
						}
					}
				}
			}
		}
	}

	return ipAddresses, nil
}

// CertConfig holds the configuration for the self-signed certificate subject.
type CertConfig struct {
	Organization  string `json:"organization"`
	Country       string `json:"country"`
	Province      string `json:"province"`
	Locality      string `json:"locality"`
	StreetAddress string `json:"street_address"`
	PostalCode    string `json:"postal_code"`
	CommonName    string `json:"common_name"`
}

// GenerateSelfSignedCert generates a self-signed TLS certificate
// that includes localhost and all local IP addresses.
func GenerateSelfSignedCert(listenAddr string, config CertConfig, logger *slog.Logger) (*tls.Certificate, error) {
	// Generate private key
	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		if logger != nil {
			logger.Error("failed to generate RSA key", "error", err)
		}
		return nil, fmt.Errorf("failed to generate RSA key: %w", err)
	}

	// Get all local IP addresses
	ipAddresses, err := GetLocalIPAddresses(listenAddr)
	if err != nil {
		return nil, fmt.Errorf("failed to get local IP addresses: %w", err)
	}

	// Create certificate template with random serial number
	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
	if err != nil {
		if logger != nil {
			logger.Error("failed to generate serial number", "error", err)
		}
		return nil, fmt.Errorf("failed to generate serial number: %w", err)
	}

	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject: pkix.Name{
			Organization:  []string{config.Organization},
			Country:       []string{config.Country},
			Province:      []string{config.Province},
			Locality:      []string{config.Locality},
			StreetAddress: []string{config.StreetAddress},
			PostalCode:    []string{config.PostalCode},
			CommonName:    config.CommonName,
		},
		NotBefore:             time.Now(),
		NotAfter:              time.Now().Add(CertificateValidityDuration),
		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
		IPAddresses:           ipAddresses,
		DNSNames:              []string{"localhost"},
	}

	// Create certificate
	certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
	if err != nil {
		if logger != nil {
			logger.Error("failed to create certificate", "error", err)
		}
		return nil, fmt.Errorf("failed to create certificate: %w", err)
	}

	// Convert to tls.Certificate
	cert := tls.Certificate{
		Certificate: [][]byte{certDER},
		PrivateKey:  privateKey,
	}

	if logger != nil {
		logger.Info("generated self-signed TLS certificate")
	}
	return &cert, nil
}