Skip to content

Commit

Permalink
ssl: look for ~/.postgresql/root.crt
Browse files Browse the repository at this point in the history
If the sslrootcert option hasn't been specified, use
~/.postgresql/root.crt if it exists (or %APPDATA%\postgresql\root.crt on
Windows).

This is what libpq does. See
- https://www.postgresql.org/docs/11/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT
- https://www.postgresql.org/docs/current/libpq-ssl.html#LIBQ-SSL-CERTIFICATES
  • Loading branch information
pschultz committed Jun 28, 2023
1 parent 300ec9b commit 2f89da5
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 7 deletions.
34 changes: 27 additions & 7 deletions ssl.go
Expand Up @@ -8,14 +8,39 @@ import (
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
)

var testUser *user.User // for replacing user.Current() in tests

// ssl generates a function to upgrade a net.Conn based on the "sslmode" and
// related settings. The function is nil when no upgrade should take place.
func ssl(o values) (func(net.Conn) (net.Conn, error), error) {
var usr *user.User
// usr.Current() might fail when cross-compiling. We have to ignore the
// error and continue without home directory defaults, since we wouldn't
// know from where to load certificates.
if testUser != nil {
usr = new(user.User)
*usr = *testUser
} else {
usr, _ = user.Current()
}

verifyCaOnly := false
tlsConf := tls.Config{}

if usr != nil && o["sslmode"] != "disable" && o["sslrootcert"] == "" {
// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT
// https://www.postgresql.org/docs/current/libpq-ssl.html#LIBQ-SSL-CERTIFICATES
if runtime.GOOS == "windows" {
o["sslrootcert"] = filepath.Join(usr.HomeDir, "AppData", "Roaming", "postgresql", "root.crt")
} else {
o["sslrootcert"] = filepath.Join(usr.HomeDir, ".postgresql", "root.crt")
}
}

switch mode := o["sslmode"]; mode {
// "require" is the default.
case "", "require":
Expand Down Expand Up @@ -61,7 +86,7 @@ func ssl(o values) (func(net.Conn) (net.Conn, error), error) {
tlsConf.ServerName = o["host"]
}

err := sslClientCertificates(&tlsConf, o)
err := sslClientCertificates(&tlsConf, o, usr)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -93,7 +118,7 @@ func ssl(o values) (func(net.Conn) (net.Conn, error), error) {
// "sslkey" settings, or if they aren't set, from the .postgresql directory
// in the user's home directory. The configured files must exist and have
// the correct permissions.
func sslClientCertificates(tlsConf *tls.Config, o values) error {
func sslClientCertificates(tlsConf *tls.Config, o values, user *user.User) error {
sslinline := o["sslinline"]
if sslinline == "true" {
cert, err := tls.X509KeyPair([]byte(o["sslcert"]), []byte(o["sslkey"]))
Expand All @@ -104,11 +129,6 @@ func sslClientCertificates(tlsConf *tls.Config, o values) error {
return nil
}

// user.Current() might fail when cross-compiling. We have to ignore the
// error and continue without home directory defaults, since we wouldn't
// know from where to load them.
user, _ := user.Current()

// In libpq, the client certificate is only loaded if the setting is not blank.
//
// https://github.com/postgres/postgres/blob/REL9_6_2/src/interfaces/libpq/fe-secure-openssl.c#L1036-L1037
Expand Down
106 changes: 106 additions & 0 deletions ssl_test.go
Expand Up @@ -4,6 +4,7 @@ package pq

import (
"bytes"
"context"
_ "crypto/sha256"
"crypto/tls"
"crypto/x509"
Expand All @@ -13,6 +14,7 @@ import (
"io"
"net"
"os"
"os/user"
"path/filepath"
"strings"
"testing"
Expand Down Expand Up @@ -377,6 +379,110 @@ func TestSNISupport(t *testing.T) {
}
}

func TestDefaultRootCert(t *testing.T) {
homeDir, err := setupHomeWithRootCRT(t)
if err != nil {
t.Fatalf("setup mock $HOME: %v", err)
}

testUser = &user.User{
// no leading slash to we can be sure that $HOME/.postgresql/root.crt
// does not exist
HomeDir: homeDir,
}
defer func() { testUser = nil }()

o := values{"sslmode": "verify-ca"}

upgrade, err := ssl(o)
if err != nil {
t.Fatal(err)
}

addr, handshakeErr := mockTLSServer(t, "certs/server.crt", "certs/server.key")

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", addr)
if err != nil {
t.Fatal(err)
}
defer conn.Close()

if _, err := upgrade(conn); err != nil {
t.Fatal(err)
}

select {
case <-ctx.Done():
t.Fatal(ctx.Err())
case err := <-handshakeErr:
if err != nil {
t.Fatal(err)
}
}
}

func setupHomeWithRootCRT(t *testing.T) (string, error) {
t.Helper()

homeDir, err := os.MkdirTemp("", "lib-pg-ssl-test-*")

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (10, 1.15)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (12, 1.15)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (12, 1.14)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (10, 1.14)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (11, 1.14)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (11, 1.15)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (13, 1.15)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (13, 1.14)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (14, 1.14)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (15, 1.15)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (15, 1.14)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (14, 1.15)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (9.6, 1.15)

undefined: os.MkdirTemp

Check failure on line 430 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (9.6, 1.14)

undefined: os.MkdirTemp
if err != nil {
return "", err
}
t.Cleanup(func() { os.RemoveAll(homeDir) })

err = os.MkdirAll(filepath.Join(homeDir, ".postgresql"), 0700)
if err != nil {
return "", err
}

b, err := os.ReadFile("certs/root.crt")

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (10, 1.15)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (12, 1.15)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (12, 1.14)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (10, 1.14)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (11, 1.14)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (11, 1.15)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (13, 1.15)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (13, 1.14)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (14, 1.14)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (15, 1.15)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (15, 1.14)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (14, 1.15)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (9.6, 1.15)

undefined: os.ReadFile

Check failure on line 441 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (9.6, 1.14)

undefined: os.ReadFile
if err != nil {
return "", err
}

err = os.WriteFile(filepath.Join(homeDir, ".postgresql", "root.crt"), b, 0600)

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (10, 1.15)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (12, 1.15)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (12, 1.14)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (10, 1.14)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (11, 1.14)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (11, 1.15)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (13, 1.15)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (13, 1.14)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (14, 1.14)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (15, 1.15)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (15, 1.14)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (14, 1.15)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (9.6, 1.15)

undefined: os.WriteFile

Check failure on line 446 in ssl_test.go

View workflow job for this annotation

GitHub Actions / test (9.6, 1.14)

undefined: os.WriteFile
if err != nil {
return "", err
}

return homeDir, nil
}

func mockTLSServer(t *testing.T, certFile, keyFile string) (string, chan error) {
t.Helper()

ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ln.Close() })

serverCert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
t.Fatal(err)
}
serverConf := &tls.Config{
Certificates: []tls.Certificate{serverCert},
}

handshakeErr := make(chan error, 1)
go func() {
conn, err := ln.Accept()
if err != nil {
t.Logf("mockTLSServer: cannot Accept: %v", err)
return
}
defer conn.Close()

handshakeErr <- tls.Server(conn, serverConf).Handshake()
}()

return ln.Addr().String(), handshakeErr
}

// Make a postgres mock server to test TLS SNI
//
// Accepts postgres StartupMessage and handles TLS clientHello, then closes a connection.
Expand Down

0 comments on commit 2f89da5

Please sign in to comment.