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