146 lines
4.3 KiB
Go
146 lines
4.3 KiB
Go
// 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,
|
|
}
|
|
}
|