// Package tlsutil provides certificate loading: LoadOrGenerate for the // file/self-signed paths (this file), and ACMEManager (acme_manager.go) for // real Let's Encrypt-style issuance via internal/acme. The self-signed // generator here remains the fallback for tls.mode "off" or "file" without // a cert on disk yet — genuinely necessary for local dev/testing, not a // placeholder for a missing feature. package tlsutil import ( "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "fmt" "log/slog" "math/big" "net" "time" ) // LoadOrGenerate returns a tls.Config for the given mode: // - "file": load cert/key from disk paths // - anything else ("acme" not yet implemented, "off"): generate a self-signed // cert so STARTTLS/IMAPS/etc. still work during development. Logs a loud // warning since this is never appropriate for production. func LoadOrGenerate(mode, hostname, certFile, keyFile string, minVersion uint16) (*tls.Config, error) { var cert tls.Certificate var err error switch mode { case "file": cert, err = tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, fmt.Errorf("loading TLS cert/key: %w", err) } case "acme": // Reaching here (rather than the real ACMEManager path in main.go) // means mode=="acme" but no acme_domains were configured — a real // ACME client exists (internal/acme, wired in main.go), it's just // not usable without knowing which domain(s) to request a cert for. slog.Warn("TLS mode is 'acme' but no acme_domains are configured — "+ "generating a SELF-SIGNED certificate instead. Set tls.acme_domains "+ "in config.yaml to enable real Let's Encrypt issuance.", "hostname", hostname) cert, err = generateSelfSigned(hostname) if err != nil { return nil, fmt.Errorf("generating self-signed cert: %w", err) } default: slog.Warn("TLS mode is 'off' — generating a SELF-SIGNED certificate. "+ "This is fine for local testing but MUST NOT be used in production; "+ "set tls.mode to 'acme' (with acme_domains configured) or 'file'.", "mode", mode, "hostname", hostname) cert, err = generateSelfSigned(hostname) if err != nil { return nil, fmt.Errorf("generating self-signed cert: %w", err) } } return &tls.Config{ Certificates: []tls.Certificate{cert}, MinVersion: minVersion, ServerName: hostname, }, nil } // ParseMinVersion converts the config string ("TLS12"/"TLS13") to the // crypto/tls constant. func ParseMinVersion(s string) uint16 { if s == "TLS13" { return tls.VersionTLS13 } return tls.VersionTLS12 } func generateSelfSigned(hostname string) (tls.Certificate, error) { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return tls.Certificate{}, err } serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return tls.Certificate{}, err } template := x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{CommonName: hostname, Organization: []string{"GoMail (self-signed, dev only)"}}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().AddDate(1, 0, 0), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, IsCA: true, BasicConstraintsValid: true, } if ip := net.ParseIP(hostname); ip != nil { template.IPAddresses = []net.IP{ip} } else { template.DNSNames = []string{hostname} } derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { return tls.Certificate{}, err } return tls.Certificate{ Certificate: [][]byte{derBytes}, PrivateKey: priv, }, nil }