Files
gomail/internal/tlsutil/selfsigned.go
T

115 lines
3.7 KiB
Go
Raw Normal View History

2026-08-09 18:03:09 +01:00
// 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
}