Add support for TLS listeners

Support for -tls-cert and -tls-key to point to a certificate and
key to encrypt communications between the user and hydroxide.

Support for -tls-client-ca to ask client to present a certificate
signed by the provided CA.

Closes: https://github.com/emersion/hydroxide/issues/13
This commit is contained in:
primalmotion 2021-02-24 00:48:29 -08:00 committed by GitHub
parent b812444ff8
commit eaed359829
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 85 additions and 14 deletions

View File

@ -3,6 +3,7 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"crypto/tls"
"flag" "flag"
"fmt" "fmt"
"io" "io"
@ -21,6 +22,7 @@ import (
"github.com/emersion/hydroxide/auth" "github.com/emersion/hydroxide/auth"
"github.com/emersion/hydroxide/carddav" "github.com/emersion/hydroxide/carddav"
"github.com/emersion/hydroxide/config"
"github.com/emersion/hydroxide/events" "github.com/emersion/hydroxide/events"
"github.com/emersion/hydroxide/exports" "github.com/emersion/hydroxide/exports"
imapbackend "github.com/emersion/hydroxide/imap" imapbackend "github.com/emersion/hydroxide/imap"
@ -39,25 +41,32 @@ func newClient() *protonmail.Client {
} }
} }
func listenAndServeSMTP(addr string, debug bool, authManager *auth.Manager) error { func listenAndServeSMTP(addr string, debug bool, authManager *auth.Manager, tlsConfig *tls.Config) error {
be := smtpbackend.New(authManager) be := smtpbackend.New(authManager)
s := smtp.NewServer(be) s := smtp.NewServer(be)
s.Addr = addr s.Addr = addr
s.Domain = "localhost" // TODO: make this configurable s.Domain = "localhost" // TODO: make this configurable
s.AllowInsecureAuth = true // TODO: remove this s.AllowInsecureAuth = tlsConfig == nil
s.TLSConfig = tlsConfig
if debug { if debug {
s.Debug = os.Stdout s.Debug = os.Stdout
} }
if s.TLSConfig != nil {
log.Println("SMTP server listening with TLS on", s.Addr)
return s.ListenAndServeTLS()
}
log.Println("SMTP server listening on", s.Addr) log.Println("SMTP server listening on", s.Addr)
return s.ListenAndServe() return s.ListenAndServe()
} }
func listenAndServeIMAP(addr string, debug bool, authManager *auth.Manager, eventsManager *events.Manager) error { func listenAndServeIMAP(addr string, debug bool, authManager *auth.Manager, eventsManager *events.Manager, tlsConfig *tls.Config) error {
be := imapbackend.New(authManager, eventsManager) be := imapbackend.New(authManager, eventsManager)
s := imapserver.New(be) s := imapserver.New(be)
s.Addr = addr s.Addr = addr
s.AllowInsecureAuth = true // TODO: remove this s.AllowInsecureAuth = tlsConfig == nil
s.TLSConfig = tlsConfig
if debug { if debug {
s.Debug = os.Stdout s.Debug = os.Stdout
} }
@ -65,15 +74,21 @@ func listenAndServeIMAP(addr string, debug bool, authManager *auth.Manager, even
s.Enable(imapspacialuse.NewExtension()) s.Enable(imapspacialuse.NewExtension())
s.Enable(imapmove.NewExtension()) s.Enable(imapmove.NewExtension())
if s.TLSConfig != nil {
log.Println("IMAP server listening with TLS on", s.Addr)
return s.ListenAndServeTLS()
}
log.Println("IMAP server listening on", s.Addr) log.Println("IMAP server listening on", s.Addr)
return s.ListenAndServe() return s.ListenAndServe()
} }
func listenAndServeCardDAV(addr string, authManager *auth.Manager, eventsManager *events.Manager) error { func listenAndServeCardDAV(addr string, authManager *auth.Manager, eventsManager *events.Manager, tlsConfig *tls.Config) error {
handlers := make(map[string]http.Handler) handlers := make(map[string]http.Handler)
s := &http.Server{ s := &http.Server{
Addr: addr, Addr: addr,
TLSConfig: tlsConfig,
Handler: http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { Handler: http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("WWW-Authenticate", "Basic") resp.Header().Set("WWW-Authenticate", "Basic")
@ -108,6 +123,11 @@ func listenAndServeCardDAV(addr string, authManager *auth.Manager, eventsManager
}), }),
} }
if s.TLSConfig != nil {
log.Println("CardDAV server listening with TLS on", s.Addr)
return s.ListenAndServeTLS("", "")
}
log.Println("CardDAV server listening on", s.Addr) log.Println("CardDAV server listening on", s.Addr)
return s.ListenAndServe() return s.ListenAndServe()
} }
@ -147,7 +167,13 @@ Global options:
-imap-port example.com -imap-port example.com
IMAP port on which hydroxide listens, defaults to 1143 IMAP port on which hydroxide listens, defaults to 1143
-carddav-port example.com -carddav-port example.com
CardDAV port on which hydroxide listens, defaults to 8080` CardDAV port on which hydroxide listens, defaults to 8080
-tls-cert /path/to/cert.pem
Path to the certificate to use for incoming connections (Optional)
-tls-key /path/to/key.pem
Path to the certificate key to use for incoming connections (Optional)
-tls-client-ca /path/to/ca.pem
If set, clients must provide a certificate signed by the given CA (Optional)`
func main() { func main() {
flag.BoolVar(&debug, "debug", false, "Enable debug logs") flag.BoolVar(&debug, "debug", false, "Enable debug logs")
@ -161,6 +187,10 @@ func main() {
carddavHost := flag.String("carddav-host", "127.0.0.1", "Allowed CardDAV email hostname on which hydroxide listens, defaults to 127.0.0.1") carddavHost := flag.String("carddav-host", "127.0.0.1", "Allowed CardDAV email hostname on which hydroxide listens, defaults to 127.0.0.1")
carddavPort := flag.String("carddav-port", "8080", "CardDAV port on which hydroxide listens, defaults to 8080") carddavPort := flag.String("carddav-port", "8080", "CardDAV port on which hydroxide listens, defaults to 8080")
tlsCert := flag.String("tls-cert", "", "Path to the certificate to use for incoming connections")
tlsCertKey := flag.String("tls-key", "", "Path to the certificate key to use for incoming connections")
tlsClientCA := flag.String("tls-client-ca", "", "If set, clients must provide a certificate signed by the given CA")
authCmd := flag.NewFlagSet("auth", flag.ExitOnError) authCmd := flag.NewFlagSet("auth", flag.ExitOnError)
exportSecretKeysCmd := flag.NewFlagSet("export-secret-keys", flag.ExitOnError) exportSecretKeysCmd := flag.NewFlagSet("export-secret-keys", flag.ExitOnError)
importMessagesCmd := flag.NewFlagSet("import-messages", flag.ExitOnError) importMessagesCmd := flag.NewFlagSet("import-messages", flag.ExitOnError)
@ -172,6 +202,11 @@ func main() {
flag.Parse() flag.Parse()
tlsConfig, err := config.TLS(*tlsCert, *tlsCertKey, *tlsClientCA)
if err != nil {
log.Fatal(err)
}
cmd := flag.Arg(0) cmd := flag.Arg(0)
switch cmd { switch cmd {
case "auth": case "auth":
@ -412,17 +447,17 @@ func main() {
case "smtp": case "smtp":
addr := *smtpHost + ":" + *smtpPort addr := *smtpHost + ":" + *smtpPort
authManager := auth.NewManager(newClient) authManager := auth.NewManager(newClient)
log.Fatal(listenAndServeSMTP(addr, debug, authManager)) log.Fatal(listenAndServeSMTP(addr, debug, authManager, tlsConfig))
case "imap": case "imap":
addr := *imapHost + ":" + *imapPort addr := *imapHost + ":" + *imapPort
authManager := auth.NewManager(newClient) authManager := auth.NewManager(newClient)
eventsManager := events.NewManager() eventsManager := events.NewManager()
log.Fatal(listenAndServeIMAP(addr, debug, authManager, eventsManager)) log.Fatal(listenAndServeIMAP(addr, debug, authManager, eventsManager, tlsConfig))
case "carddav": case "carddav":
addr := *carddavHost + ":" + *carddavPort addr := *carddavHost + ":" + *carddavPort
authManager := auth.NewManager(newClient) authManager := auth.NewManager(newClient)
eventsManager := events.NewManager() eventsManager := events.NewManager()
log.Fatal(listenAndServeCardDAV(addr, authManager, eventsManager)) log.Fatal(listenAndServeCardDAV(addr, authManager, eventsManager, tlsConfig))
case "serve": case "serve":
smtpAddr := *smtpHost + ":" + *smtpPort smtpAddr := *smtpHost + ":" + *smtpPort
imapAddr := *imapHost + ":" + *imapPort imapAddr := *imapHost + ":" + *imapPort
@ -433,13 +468,13 @@ func main() {
done := make(chan error, 3) done := make(chan error, 3)
go func() { go func() {
done <- listenAndServeSMTP(smtpAddr, debug, authManager) done <- listenAndServeSMTP(smtpAddr, debug, authManager, tlsConfig)
}() }()
go func() { go func() {
done <- listenAndServeIMAP(imapAddr, debug, authManager, eventsManager) done <- listenAndServeIMAP(imapAddr, debug, authManager, eventsManager, tlsConfig)
}() }()
go func() { go func() {
done <- listenAndServeCardDAV(carddavAddr, authManager, eventsManager) done <- listenAndServeCardDAV(carddavAddr, authManager, eventsManager, tlsConfig)
}() }()
log.Fatal(<-done) log.Fatal(<-done)
default: default:

36
config/tls.go Normal file
View File

@ -0,0 +1,36 @@
package config
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
)
func TLS(certPath string, keyPath string, clientCAPath string) (*tls.Config, error) {
tlsConfig := &tls.Config{}
if certPath != "" && keyPath != "" {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, fmt.Errorf("error: unable load key pair: %s", err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
if clientCAPath != "" {
data, err := ioutil.ReadFile(clientCAPath)
if err != nil {
return nil, fmt.Errorf("error: unable read CA file: %s", err)
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(data)
tlsConfig.ClientCAs = pool
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
}
return tlsConfig, nil
}