~sircmpwn/aerc

support per-acct TLS client certs & CA certs v2 PROPOSED

Elizabeth Hollyburn: 1
 support per-acct TLS client certs & CA certs

 4 files changed, 110 insertions(+), 24 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/aerc/patches/25288/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH v2] support per-acct TLS client certs & CA certs Export this patch

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..8485461 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.CAPath = 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