~sircmpwn/aerc

[PATCH] support per-acct TLS client certs & CA certs

Details
Message ID
<20210917162044.21465-1-git@hollyburn.io>
DKIM signature
pass
Download raw message
config: optionally accept account-specific paths for client certs and
keys; and an additional certificate authority file, which will be
appended to the system CA pool for TLS connections for that account

worker/imap, command/compose, lib: functions which open (or upgrade to)
a TLS connection will take a *tls.Config, which can be built with a new
function in lib/tls.go from fields in an AccountConfig
---
 commands/compose/send.go | 55 ++++++++++++++++++++++++----------------
 config/config.go         |  9 +++++++
 lib/tls.go               | 55 ++++++++++++++++++++++++++++++++++++++++
 worker/imap/worker.go    | 15 +++++++++--
 4 files changed, 110 insertions(+), 24 deletions(-)
 create mode 100644 lib/tls.go

diff --git a/commands/compose/send.go b/commands/compose/send.go
index 849182d..b9d3e58 100644
--- a/commands/compose/send.go
+++ b/commands/compose/send.go
@@ -81,13 +81,25 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
 	if starttls_, ok := config.Params["smtp-starttls"]; ok {
 		starttls = starttls_ == "yes"
 	}
+	tlsConfig, err := lib.BuildTLSConfig(
+		config.ClientCertPath,
+		config.ClientKeyPath,
+		config.CAPath,
+	)
+	if err != nil {
+		return err
+	}
+	if tlsConfig == nil {
+		tlsConfig = &tls.Config{}
+	}
 	ctx := sendCtx{
-		uri:      uri,
-		scheme:   scheme,
-		auth:     auth,
-		starttls: starttls,
-		from:     from,
-		rcpts:    rcpts,
+		uri:       uri,
+		scheme:    scheme,
+		auth:      auth,
+		starttls:  starttls,
+		from:      from,
+		rcpts:     rcpts,
+		tlsConfig: tlsConfig,
 	}
 
 	// we don't want to block the UI thread while we are sending
@@ -172,12 +184,13 @@ func listRecipients(h *mail.Header) ([]*mail.Address, error) {
 }
 
 type sendCtx struct {
-	uri      *url.URL
-	scheme   string
-	auth     string
-	starttls bool
-	from     *mail.Address
-	rcpts    []*mail.Address
+	uri       *url.URL
+	scheme    string
+	auth      string
+	starttls  bool
+	from      *mail.Address
+	rcpts     []*mail.Address
+	tlsConfig *tls.Config
 }
 
 func newSendmailSender(ctx sendCtx) (io.WriteCloser, error) {
@@ -313,9 +326,9 @@ func newSmtpSender(ctx sendCtx) (io.WriteCloser, error) {
 	)
 	switch ctx.scheme {
 	case "smtp":
-		conn, err = connectSmtp(ctx.starttls, ctx.uri.Host)
+		conn, err = connectSmtp(ctx.tlsConfig, ctx.starttls, ctx.uri.Host)
 	case "smtps":
-		conn, err = connectSmtps(ctx.uri.Host)
+		conn, err = connectSmtps(ctx.tlsConfig, ctx.uri.Host)
 	default:
 		return nil, fmt.Errorf("not an smtp protocol %s", ctx.scheme)
 	}
@@ -357,7 +370,7 @@ func newSmtpSender(ctx sendCtx) (io.WriteCloser, error) {
 	return s.w, nil
 }
 
-func connectSmtp(starttls bool, host string) (*smtp.Client, error) {
+func connectSmtp(tlsConfig *tls.Config, starttls bool, host string) (*smtp.Client, error) {
 	serverName := host
 	if !strings.ContainsRune(host, ':') {
 		host = host + ":587" // Default to submission port
@@ -376,9 +389,8 @@ func connectSmtp(starttls bool, host string) (*smtp.Client, error) {
 			conn.Close()
 			return nil, err
 		}
-		if err = conn.StartTLS(&tls.Config{
-			ServerName: serverName,
-		}); err != nil {
+		tlsConfig.ServerName = serverName
+		if err = conn.StartTLS(tlsConfig); err != nil {
 			conn.Close()
 			return nil, errors.Wrap(err, "StartTLS")
 		}
@@ -394,16 +406,15 @@ func connectSmtp(starttls bool, host string) (*smtp.Client, error) {
 	return conn, nil
 }
 
-func connectSmtps(host string) (*smtp.Client, error) {
+func connectSmtps(tlsConfig *tls.Config, host string) (*smtp.Client, error) {
 	serverName := host
 	if !strings.ContainsRune(host, ':') {
 		host = host + ":465" // Default to smtps port
 	} else {
 		serverName = host[:strings.IndexRune(host, ':')]
 	}
-	conn, err := smtp.DialTLS(host, &tls.Config{
-		ServerName: serverName,
-	})
+	tlsConfig.ServerName = serverName
+	conn, err := smtp.DialTLS(host, tlsConfig)
 	if err != nil {
 		return nil, errors.Wrap(err, "smtp.DialTLS")
 	}
diff --git a/config/config.go b/config/config.go
index af9c63b..983360b 100644
--- a/config/config.go
+++ b/config/config.go
@@ -87,6 +87,9 @@ type AccountConfig struct {
 	SignatureFile   string
 	SignatureCmd    string
 	FoldersSort     []string `ini:"folders-sort" delim:","`
+	ClientCertPath  string
+	ClientKeyPath   string
+	CAPath          string
 }
 
 type BindingConfig struct {
@@ -209,6 +212,12 @@ func loadAccountConfig(path string) ([]AccountConfig, error) {
 				account.CopyTo = val
 			} else if key == "archive" {
 				account.Archive = val
+			} else if key == "client-cert" {
+				account.ClientCertPath = val
+			} else if key == "client-key" {
+				account.ClientKeyPath = val
+			} else if key == "ca-file" {
+				account.RootCAPath = val
 			} else if key != "name" {
 				account.Params[key] = val
 			}
diff --git a/lib/tls.go b/lib/tls.go
new file mode 100644
index 0000000..d3304b0
--- /dev/null
+++ b/lib/tls.go
@@ -0,0 +1,55 @@
+package lib
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"errors"
+	"io"
+	"os"
+)
+
+var errFailedToLoadCA = errors.New("Couldn't load root CA. Malformed PEM?")
+
+func BuildTLSConfig(certPath string, keyPath string, caPath string) (*tls.Config, error) {
+	if certPath == "" && caPath == "" {
+		return nil, nil
+	}
+
+	tlsConfig := &tls.Config{}
+
+	if certPath != "" {
+		if keyPath == "" {
+			keyPath = certPath
+		}
+		cert, err := tls.LoadX509KeyPair(certPath, keyPath)
+		if err != nil {
+			return nil, err
+		}
+
+		tlsConfig.Certificates = []tls.Certificate{cert}
+	}
+
+	if caPath != "" {
+		rootCAs, err := x509.SystemCertPool()
+		if err != nil {
+			// TODO: log if system cert pool can't be loaded
+			rootCAs = x509.NewCertPool()
+		}
+		rootCAFile, err := os.Open(caPath)
+		if err != nil {
+			return nil, err
+		}
+		rootCA, err := io.ReadAll(rootCAFile)
+		if err != nil {
+			return nil, err
+		}
+		success := rootCAs.AppendCertsFromPEM(rootCA)
+		if !success {
+			return nil, errFailedToLoadCA
+		}
+
+		tlsConfig.RootCAs = rootCAs
+	}
+
+	return tlsConfig, nil
+}
diff --git a/worker/imap/worker.go b/worker/imap/worker.go
index dab0afb..6972013 100644
--- a/worker/imap/worker.go
+++ b/worker/imap/worker.go
@@ -39,6 +39,7 @@ type IMAPWorker struct {
 		user        *url.Userinfo
 		folders     []string
 		oauthBearer lib.OAuthBearer
+		tlsConfig   *tls.Config
 	}
 
 	client   *imapClient
@@ -107,11 +108,21 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
 
 		w.config.user = u.User
 		w.config.folders = msg.Config.Folders
+
+		w.config.tlsConfig, err = lib.BuildTLSConfig(
+			msg.Config.ClientCertPath,
+			msg.Config.ClientKeyPath,
+			msg.Config.CAPath,
+		)
+		if err != nil {
+			return err
+		}
 	case *types.Connect:
 		var (
 			c   *client.Client
 			err error
 		)
 		switch w.config.scheme {
 		case "imap":
 			c, err = client.Dial(w.config.addr)
@@ -120,12 +131,12 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
 			}
 
 			if !w.config.insecure {
-				if err := c.StartTLS(&tls.Config{}); err != nil {
+				if err := c.StartTLS(w.config.tlsConfig); err != nil {
 					return err
 				}
 			}
 		case "imaps":
-			c, err = client.DialTLS(w.config.addr, &tls.Config{})
+			c, err = client.DialTLS(w.config.addr, w.config.tlsConfig)
 			if err != nil {
 				return err
 			}
2.33.0
Reply to thread Export thread (mbox)