115 lines
3.7 KiB
Go
115 lines
3.7 KiB
Go
// 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
|
|
}
|