// Package tlsutil generates the self-signed certificate used by the direct-TLS SMTP // listener and builds its tls.Config, mirroring email_server/tls_utils.py. package tlsutil import ( "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" "os" "path/filepath" "sync" "time" ) // GenerateSelfSignedCert mirrors tls_utils.generate_self_signed_cert: skips generation // if both files already exist; otherwise writes an RSA-2048/SHA-256, 1-year-valid, // self-signed cert with the same subject fields as the Python version. func GenerateSelfSignedCert(certFile, keyFile string) error { if _, err := os.Stat(certFile); err == nil { if _, err := os.Stat(keyFile); err == nil { return nil } } if err := os.MkdirAll(filepath.Dir(certFile), 0o755); err != nil { return err } if err := os.MkdirAll(filepath.Dir(keyFile), 0o755); err != nil { return err } priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return err } subject := pkix.Name{ CommonName: "localhost", Organization: []string{"PyMTA Server"}, Country: []string{"GB"}, } template := x509.Certificate{ SerialNumber: big.NewInt(1000), Subject: subject, Issuer: subject, NotBefore: time.Now(), NotAfter: time.Now().Add(365 * 24 * time.Hour), SignatureAlgorithm: x509.SHA256WithRSA, BasicConstraintsValid: true, } certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { return err } certOut, err := os.Create(certFile) if err != nil { return err } defer certOut.Close() if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil { return err } keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) if err != nil { return err } defer keyOut.Close() keyDER, err := x509.MarshalPKCS8PrivateKey(priv) if err != nil { return err } return pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) } // CreateSSLContext mirrors tls_utils.create_ssl_context: loads the cert/key pair and // pins MinVersion to TLS 1.2 (Python's ssl.create_default_context leaves this to the // environment's OpenSSL defaults, which is typically TLS 1.2+ on modern systems — // pinning it explicitly here is the closest deterministic equivalent). Cipher suites // are left at Go's own secure defaults, matching the Python code's "DEFAULT" relaxation. func CreateSSLContext(certFile, keyFile string) (*tls.Config, error) { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, err } return &tls.Config{ Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12, }, nil } // CertReloader holds the currently-active certificate behind a tls.Config's // GetCertificate hook, so a listener can pick up a newly-obtained/renewed certificate // (see internal/acmecert) without restarting the process. type CertReloader struct { certFile, keyFile string mu sync.RWMutex cert *tls.Certificate } // NewCertReloader loads certFile/keyFile once and returns a reloader ready to hand to // a tls.Config's GetCertificate field. func NewCertReloader(certFile, keyFile string) (*CertReloader, error) { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, err } return &CertReloader{certFile: certFile, keyFile: keyFile, cert: &cert}, nil } // GetCertificate satisfies tls.Config.GetCertificate. func (r *CertReloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) { r.mu.RLock() defer r.mu.RUnlock() return r.cert, nil } // Reload re-reads certFile/keyFile from disk and atomically swaps the active // certificate. Called after a successful Let's Encrypt obtain/renew. func (r *CertReloader) Reload() error { cert, err := tls.LoadX509KeyPair(r.certFile, r.keyFile) if err != nil { return err } r.mu.Lock() r.cert = &cert r.mu.Unlock() return nil } // NewReloadableTLSConfig builds a tls.Config backed by reloader instead of a fixed // certificate — used by both the SMTP and IMAP implicit-TLS listeners so a single // Reload() call (self-signed regeneration or a Let's Encrypt renewal) updates both. func NewReloadableTLSConfig(reloader *CertReloader) *tls.Config { return &tls.Config{ GetCertificate: reloader.GetCertificate, MinVersion: tls.VersionTLS12, } }