git » chasquid » commit e138f0d

chasquid: De-couple TLS certificates from domains

author Alberto Bertogli
2016-10-01 12:54:09 UTC
committer Alberto Bertogli
2016-10-09 23:51:04 UTC
parent 04dd8b95347050d6a99d3eb52ef8aec6ff24b592

chasquid: De-couple TLS certificates from domains

Having the certificates inside the domain directory may cause some confusion,
as it's possible they're not for the same name (they should be for the MX we
serve as, not the domain itself).

So it's not a problem if we have domains with no certificates (we could be
their MX with another name), and we could have more than one certificate per
"domain" (if we act as MXs with different names).

So this patch moves the certificates out of the domains into a new certs/
directory, where we do a one-level deep lookup for the files.

While at it, change the names of the files to "fullchain.pem" and
"privkey.pem", which match the names generated by the letsencrypt client, to
make it easier to set up.  There's no general convention for these names
anyway.

chasquid.go +47 -41
test/t-01-simple_local/msmtprc +1 -1
test/t-02-exim/msmtprc +1 -1
test/t-04-aliases/msmtprc +1 -1
test/t-05-null_address/msmtprc +1 -1
test/util/generate_cert.go +4 -4
test/util/lib.sh +3 -3

diff --git a/chasquid.go b/chasquid.go
index 0121984..ddef298 100644
--- a/chasquid.go
+++ b/chasquid.go
@@ -77,18 +77,31 @@ func main() {
 	s.aliasesR.SuffixSep = conf.SuffixSeparators
 	s.aliasesR.DropChars = conf.DropCharacters
 
-	// Load domains.
-	// They live inside the config directory, so the relative path works.
-	domainDirs, err := ioutil.ReadDir("domains/")
-	if err != nil {
-		glog.Fatalf("Error reading domains/ directory: %v", err)
-	}
-	if len(domainDirs) == 0 {
-		glog.Fatalf("No domains found in config")
+	// Load certificates from "certs/<directory>/{fullchain,privkey}.pem".
+	// The structure matches letsencrypt's, to make it easier for that case.
+	glog.Infof("Loading certificates")
+	for _, info := range mustReadDir("certs/") {
+		name := info.Name()
+		glog.Infof("  %s", name)
+
+		certPath := filepath.Join("certs/", name, "fullchain.pem")
+		if _, err := os.Stat(certPath); os.IsNotExist(err) {
+			continue
+		}
+		keyPath := filepath.Join("certs/", name, "privkey.pem")
+		if _, err := os.Stat(keyPath); os.IsNotExist(err) {
+			continue
+		}
+
+		err := s.AddCerts(certPath, keyPath)
+		if err != nil {
+			glog.Fatalf("    %v", err)
+		}
 	}
 
+	// Load domains from "domains/".
 	glog.Infof("Domain config paths:")
-	for _, info := range domainDirs {
+	for _, info := range mustReadDir("domains/") {
 		name := info.Name()
 		dir := filepath.Join("domains", name)
 		loadDomain(name, dir, s)
@@ -147,7 +160,6 @@ func loadDomain(name, dir string, s *Server) {
 	glog.Infof("  %s", name)
 	s.AddDomain(name)
 	s.aliasesR.AddDomain(name)
-	s.AddCerts(dir+"/cert.pem", dir+"/key.pem")
 
 	if _, err := os.Stat(dir + "/users"); err == nil {
 		glog.Infof("    adding users")
@@ -185,6 +197,19 @@ func setupSignalHandling() {
 	}()
 }
 
+// Read a directory, which must have at least some entries.
+func mustReadDir(path string) []os.FileInfo {
+	dirs, err := ioutil.ReadDir(path)
+	if err != nil {
+		glog.Fatalf("Error reading %q directory: %v", path, err)
+	}
+	if len(dirs) == 0 {
+		glog.Fatalf("No entries found in %q", path)
+	}
+
+	return dirs
+}
+
 // Mode for a socket (listening or connection).
 // We keep them distinct, as policies can differ between them.
 type SocketMode string
@@ -201,16 +226,13 @@ type Server struct {
 	// Maximum data size.
 	MaxDataSize int64
 
-	// Certificate and key pairs.
-	certs, keys []string
-
 	// Addresses.
 	addrs map[SocketMode][]string
 
 	// Listeners (that came via systemd).
 	listeners map[SocketMode][]net.Listener
 
-	// TLS config.
+	// TLS config (including loaded certificates).
 	tlsConfig *tls.Config
 
 	// Local domains.
@@ -236,6 +258,7 @@ func NewServer() *Server {
 	return &Server{
 		addrs:          map[SocketMode][]string{},
 		listeners:      map[SocketMode][]net.Listener{},
+		tlsConfig:      &tls.Config{},
 		connTimeout:    20 * time.Minute,
 		commandTimeout: 1 * time.Minute,
 		localDomains:   &set.String{},
@@ -244,9 +267,13 @@ func NewServer() *Server {
 	}
 }
 
-func (s *Server) AddCerts(cert, key string) {
-	s.certs = append(s.certs, cert)
-	s.keys = append(s.keys, key)
+func (s *Server) AddCerts(certPath, keyPath string) error {
+	cert, err := tls.LoadX509KeyPair(certPath, keyPath)
+	if err != nil {
+		return err
+	}
+	s.tlsConfig.Certificates = append(s.tlsConfig.Certificates, cert)
+	return nil
 }
 
 func (s *Server) AddAddr(a string, m SocketMode) {
@@ -292,31 +319,10 @@ func (s *Server) periodicallyReload() {
 	}
 }
 
-func (s *Server) getTLSConfig() (*tls.Config, error) {
-	var err error
-	conf := &tls.Config{}
-
-	conf.Certificates = make([]tls.Certificate, len(s.certs))
-	for i := 0; i < len(s.certs); i++ {
-		conf.Certificates[i], err = tls.LoadX509KeyPair(s.certs[i], s.keys[i])
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	conf.BuildNameToCertificate()
-
-	return conf, nil
-}
-
 func (s *Server) ListenAndServe() {
-	var err error
-
-	// Configure TLS.
-	s.tlsConfig, err = s.getTLSConfig()
-	if err != nil {
-		glog.Fatalf("Error loading TLS config: %v", err)
-	}
+	// At this point the TLS config should be done, build the
+	// name->certificate map (used by the TLS library for SNI).
+	s.tlsConfig.BuildNameToCertificate()
 
 	for m, addrs := range s.addrs {
 		for _, addr := range addrs {
diff --git a/test/t-01-simple_local/msmtprc b/test/t-01-simple_local/msmtprc
index 949e099..acb28d1 100644
--- a/test/t-01-simple_local/msmtprc
+++ b/test/t-01-simple_local/msmtprc
@@ -4,7 +4,7 @@ host testserver
 port 1587
 
 tls on
-tls_trust_file config/domains/testserver/cert.pem
+tls_trust_file config/certs/testserver/fullchain.pem
 
 from user@testserver
 
diff --git a/test/t-02-exim/msmtprc b/test/t-02-exim/msmtprc
index f24e87c..09c75b2 100644
--- a/test/t-02-exim/msmtprc
+++ b/test/t-02-exim/msmtprc
@@ -4,7 +4,7 @@ host srv-chasquid
 port 1587
 
 tls on
-tls_trust_file config/domains/srv-chasquid/cert.pem
+tls_trust_file config/certs/srv-chasquid/fullchain.pem
 
 from user@srv-chasquid
 
diff --git a/test/t-04-aliases/msmtprc b/test/t-04-aliases/msmtprc
index 1679764..8d191e1 100644
--- a/test/t-04-aliases/msmtprc
+++ b/test/t-04-aliases/msmtprc
@@ -4,7 +4,7 @@ host testserver
 port 1587
 
 tls on
-tls_trust_file config/domains/testserver/cert.pem
+tls_trust_file config/certs/testserver/fullchain.pem
 
 from user@testserver
 
diff --git a/test/t-05-null_address/msmtprc b/test/t-05-null_address/msmtprc
index 91fab60..9322c92 100644
--- a/test/t-05-null_address/msmtprc
+++ b/test/t-05-null_address/msmtprc
@@ -4,7 +4,7 @@ host testserver
 port 1587
 
 tls on
-tls_trust_file config/domains/testserver/cert.pem
+tls_trust_file config/certs/testserver/fullchain.pem
 
 from user@testserver
 
diff --git a/test/util/generate_cert.go b/test/util/generate_cert.go
index e488792..daaff1b 100644
--- a/test/util/generate_cert.go
+++ b/test/util/generate_cert.go
@@ -142,16 +142,16 @@ func main() {
 		log.Fatalf("Failed to create certificate: %s", err)
 	}
 
-	certOut, err := os.Create("cert.pem")
+	certOut, err := os.Create("fullchain.pem")
 	if err != nil {
-		log.Fatalf("failed to open cert.pem for writing: %s", err)
+		log.Fatalf("failed to open fullchain.pem for writing: %s", err)
 	}
 	pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
 	certOut.Close()
 
-	keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+	keyOut, err := os.OpenFile("privkey.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
 	if err != nil {
-		log.Fatalf("failed to open key.pem for writing:", err)
+		log.Fatalf("failed to open privkey.pem for writing:", err)
 		return
 	}
 	pem.Encode(keyOut, pemBlockForKey(priv))
diff --git a/test/util/lib.sh b/test/util/lib.sh
index 40b16cb..8dedbf2 100644
--- a/test/util/lib.sh
+++ b/test/util/lib.sh
@@ -80,11 +80,11 @@ function wait_for_file() {
 	done
 }
 
-# Generate certs for the given domain.
+# Generate certs for the given hostname.
 function generate_certs_for() {
-	mkdir -p config/domains/${1}
+	mkdir -p config/certs/${1}/
 	(
-		cd config/domains/${1}
+		cd config/certs/${1}
 		generate_cert -ca -duration=1h -host=${1}
 	)
 }