Elizabeth Hollyburn: 1 support per-acct TLS client certs & CA certs 4 files changed, 110 insertions(+), 24 deletions(-)
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 -3Learn more about email & git
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