Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ unreleased
- Clearer error when starting a new query while pq is still processing another
query ([#1272]).

- Send intermediate CAs with client certificates, so they can be signed by an
intermediate CA ([#1267]).

[#1258]: https://github.com/lib/pq/pull/1258
[#1265]: https://github.com/lib/pq/pull/1265
[#1267]: https://github.com/lib/pq/pull/1267
[#1270]: https://github.com/lib/pq/pull/1270
[#1272]: https://github.com/lib/pq/pull/1272

Expand Down
105 changes: 86 additions & 19 deletions ssl.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package pq

import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"sync"
"syscall"
Expand Down Expand Up @@ -85,7 +88,9 @@ func ssl(cfg Config, mode SSLMode) (func(net.Conn) (net.Conn, error), error) {
// applications that need certificate validation should always use
// verify-ca or verify-full.
if cfg.SSLRootCert != "" {
if _, err := os.Stat(cfg.SSLRootCert); err == nil {
if cfg.SSLInline {
verifyCaOnly = true
} else if _, err := os.Stat(cfg.SSLRootCert); err == nil {
verifyCaOnly = true
} else {
cfg.SSLRootCert = ""
Expand Down Expand Up @@ -118,10 +123,11 @@ func ssl(cfg Config, mode SSLMode) (func(net.Conn) (net.Conn, error), error) {
if err != nil {
return nil, err
}
err = sslCertificateAuthority(tlsConf, cfg)
rootPem, err := sslCertificateAuthority(tlsConf, cfg)
if err != nil {
return nil, err
}
sslAppendIntermediates(tlsConf, cfg, rootPem)

// Accept renegotiation requests initiated by the backend.
//
Expand Down Expand Up @@ -164,7 +170,15 @@ func sslClientCertificates(tlsConf *tls.Config, cfg Config) error {
if err != nil {
return err
}
tlsConf.Certificates = []tls.Certificate{cert}
// Use GetClientCertificate instead of the Certificates field. When
// Certificates is set, Go's TLS client only sends the cert if the
// server's CertificateRequest includes a CA that issued it. When the
// client cert was signed by an intermediate CA but the server only
// advertises the root CA, Go skips sending the cert entirely.
// GetClientCertificate bypasses this filtering.
tlsConf.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
return &cert, nil
}
return nil
}

Expand Down Expand Up @@ -219,33 +233,86 @@ func sslClientCertificates(tlsConf *tls.Config, cfg Config) error {
return err
}

tlsConf.Certificates = []tls.Certificate{cert}
// Using GetClientCertificate instead of Certificates field as per comment
// above.
tlsConf.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
return &cert, nil
}
return nil
}

// sslCertificateAuthority adds the RootCA specified in the "sslrootcert" setting.
func sslCertificateAuthority(tlsConf *tls.Config, cfg Config) error {
func sslCertificateAuthority(tlsConf *tls.Config, cfg Config) ([]byte, error) {
// In libpq, the root 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#L950-L951
if sslrootcert := cfg.SSLRootCert; len(sslrootcert) > 0 {
tlsConf.RootCAs = x509.NewCertPool()
if cfg.SSLRootCert == "" {
return nil, nil
}

var cert []byte
if cfg.SSLInline {
cert = []byte(sslrootcert)
} else {
var err error
cert, err = os.ReadFile(sslrootcert)
if err != nil {
return err
}
tlsConf.RootCAs = x509.NewCertPool()

var cert []byte
if cfg.SSLInline {
cert = []byte(cfg.SSLRootCert)
} else {
var err error
cert, err = os.ReadFile(cfg.SSLRootCert)
if err != nil {
return nil, err
}
}

if !tlsConf.RootCAs.AppendCertsFromPEM(cert) {
return nil, errors.New("pq: couldn't parse pem in sslrootcert")
}
return cert, nil
}

// sslAppendIntermediates appends intermediate CA certificates from sslrootcert
// to the client certificate chain. This is needed so the server can verify the
// client cert when it was signed by an intermediate CA — without this, the TLS
// handshake only sends the leaf client cert.
func sslAppendIntermediates(tlsConf *tls.Config, cfg Config, rootPem []byte) {
if cfg.SSLRootCert == "" || tlsConf.GetClientCertificate == nil || len(rootPem) == 0 {
return
}

if !tlsConf.RootCAs.AppendCertsFromPEM(cert) {
return errors.New("pq: couldn't parse pem in sslrootcert")
var (
pemData = slices.Clone(rootPem)
intermediates [][]byte
)
for {
var block *pem.Block
block, pemData = pem.Decode(pemData)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
continue
}
// Skip self-signed root CAs; only append intermediates.
if cert.IsCA && !bytes.Equal(cert.RawIssuer, cert.RawSubject) {
intermediates = append(intermediates, block.Bytes)
}
}
if len(intermediates) == 0 {
return
}

return nil
// Wrap the existing GetClientCertificate to append intermediate certs to
// the certificate chain returned during the TLS handshake.
origGetCert := tlsConf.GetClientCertificate
tlsConf.GetClientCertificate = func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
cert, err := origGetCert(info)
if err != nil {
return cert, err
}
cert.Certificate = append(cert.Certificate, intermediates...)
return cert, nil
}
}
67 changes: 67 additions & 0 deletions ssl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,74 @@ func TestSSLClientCertificates(t *testing.T) {
}
})
}
}

func TestSSLClientCertificateIntermediate(t *testing.T) {
pqtest.SkipPgpool(t)
pqtest.SkipPgbouncer(t)

startSSLTest(t, "pqgosslcert")

err := os.Chmod("testdata/init/client_intermediate.key", 0600)
if err != nil {
t.Fatal(err)
}

tests := []struct {
name string
connect string
wantErr string
}{
{
// Client cert signed by intermediate CA, sslrootcert has
// root+intermediate bundle. The server's ssl_ca_file has only root.crt,
// so sslAppendIntermediates must send the intermediate in the TLS chain.
name: "file certs",
connect: "sslmode=require user=pqgosslcert " +
"sslrootcert=testdata/init/root+intermediate.crt " +
"sslcert=testdata/init/client_intermediate.crt " +
"sslkey=testdata/init/client_intermediate.key",
},
{
name: "inline certs",
connect: fmt.Sprintf(
"sslmode=require user=pqgosslcert sslinline=true sslrootcert='%s' sslcert='%s' sslkey='%s'",
pqtest.Read(t, "testdata/init/root+intermediate.crt"),
pqtest.Read(t, "testdata/init/client_intermediate.crt"),
pqtest.Read(t, "testdata/init/client_intermediate.key"),
),
},
{
// Without the intermediate in sslrootcert, sslAppendIntermediates has
// nothing to append, so the server can't verify the client cert chain.
name: "fails without intermediate in sslrootcert",
connect: "sslmode=require user=pqgosslcert " +
"sslrootcert=testdata/init/root.crt " +
"sslcert=testdata/init/client_intermediate.crt " +
"sslkey=testdata/init/client_intermediate.key",
wantErr: "unknown certificate authority",
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
db, err := openSSLConn(t, tt.connect)
if !pqtest.ErrorContains(err, tt.wantErr) {
t.Fatalf("wrong error\nwant: %s\nhave: %s", tt.wantErr, err)
}
if err == nil {
rows, err := db.Query("select 1")
if err != nil {
t.Fatal(err)
}
if err := rows.Close(); err != nil {
t.Fatal(err)
}
}
})
}
}

// Check that clint sends SNI data when sslsni is not disabled
Expand Down
39 changes: 36 additions & 3 deletions testdata/init/Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
.PHONY: all root-ssl server-ssl client-ssl
.PHONY: all root-ssl server-ssl client-ssl intermediate-ssl server-intermediate-ssl client-intermediate-ssl

# Rebuilds self-signed root/server/client certs/keys in a consistent way
all: root-ssl server-ssl client-ssl
rm -f .srl
all: root-ssl server-ssl client-ssl intermediate-ssl server-intermediate-ssl client-intermediate-ssl
rm -f .srl *.srl
cat root.crt intermediate.crt > root+intermediate.crt

root-ssl:
openssl req -new -sha256 -nodes -newkey rsa:2048 \
Expand Down Expand Up @@ -35,3 +36,35 @@ client-ssl:
-CA ./certs/root.crt -CAkey /tmp/root.key -CAcreateserial \
-in /tmp/postgresql.csr \
-out ./certs/postgresql.crt

intermediate-ssl:
openssl req -new -sha256 -nodes -newkey rsa:2048 \
-config intermediate.cnf \
-keyout /tmp/intermediate.key \
-out /tmp/intermediate.csr
openssl x509 -req -days 3653 -sha256 \
-extfile <(printf "basicConstraints=critical,CA:TRUE,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign\nsubjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid,issuer") \
-CA root.crt -CAkey /tmp/root.key -CAcreateserial \
-in /tmp/intermediate.csr \
-out intermediate.crt

server-intermediate-ssl:
openssl req -new -sha256 -nodes -newkey rsa:2048 \
-config server_intermediate.cnf \
-keyout server_intermediate.key \
-out /tmp/server_intermediate.csr
openssl x509 -req -days 3653 -sha256 \
-extfile server_intermediate.cnf -extensions req_ext \
-CA intermediate.crt -CAkey /tmp/intermediate.key -CAcreateserial \
-in /tmp/server_intermediate.csr \
-out server_intermediate.crt

client-intermediate-ssl:
openssl req -new -sha256 -nodes -newkey rsa:2048 \
-config client_intermediate.cnf \
-keyout client_intermediate.key \
-out /tmp/client_intermediate.csr
openssl x509 -req -days 3653 -sha256 \
-CA intermediate.crt -CAkey /tmp/intermediate.key -CAcreateserial \
-in /tmp/client_intermediate.csr \
-out client_intermediate.crt
10 changes: 10 additions & 0 deletions testdata/init/client_intermediate.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[req]
distinguished_name = req_distinguished_name
prompt = no

[req_distinguished_name]
C = US
ST = Nevada
L = Las Vegas
O = github.com/lib/pq
CN = pqgosslcert
22 changes: 22 additions & 0 deletions testdata/init/client_intermediate.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDnzCCAoegAwIBAgIUMmQ+3iTA688OIt+AIz7OF87gohkwDQYJKoZIhvcNAQEL
BQAwazELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk5ldmFkYTESMBAGA1UEBwwJTGFz
IFZlZ2FzMRowGAYDVQQKDBFnaXRodWIuY29tL2xpYi9wcTEbMBkGA1UEAwwScHEg
SW50ZXJtZWRpYXRlIENBMB4XDTI2MDMwNTE3NDkwNloXDTM2MDMwNTE3NDkwNlow
ZDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk5ldmFkYTESMBAGA1UEBwwJTGFzIFZl
Z2FzMRowGAYDVQQKDBFnaXRodWIuY29tL2xpYi9wcTEUMBIGA1UEAwwLcHFnb3Nz
bGNlcnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hDnWJaHuDWMf
Q7JCDGTZuqdFfiP97SPT6ca4a2iWdmttfUhoBkrjrpAmg4z3YHU+ogKyqZtJ7gEh
y+GyntyPEmL8Rua4/5QhW17xzeDqaVbj231azUfeVjbZlqH/ysN+3rVt8rvFlFRs
p0QROqYBClMZNzDBBpSw6hyU0dVNBSMccHGa37xN8jILtq+NI9wVatIRdM/DqTYN
4NmwOdvlJ6cmoyhzfLl1lKVZvxBsj8JVjc/RlK0IvXoGKKQBILk257yEITzkrVT4
swCBbptc+wTtMkLrxYYVF75HqQa/F33gsPP9GnWL9kY48gWI3yHl97gyarQe9DkK
idOsuBO7AgMBAAGjQjBAMB0GA1UdDgQWBBT5l0kq3uJh5GYbxsF1StUr9NauJTAf
BgNVHSMEGDAWgBSMTYV4MVWf9hD/cI3IJWluimVfXTANBgkqhkiG9w0BAQsFAAOC
AQEAjZGXps9DHxJZB2Tg3MafmSu9mXxfSXKFuorSA0vaLyCsUp/ZBKq1o+mh9KSC
iho2/Ya6R/ZFt3yuzgRicPHd+3kNlmFwgtg8OX5WxCGpDIb6TjHoRDLCm9VKxvF5
Fc2FSJVopzaRdhYxHr2SXe86vRExQTHwo0L/dMgQZswitg54RR0E2qSZaw5E/yk/
aBKd9OeZKrjCSwnx1CtW4QMQIjkoE+VcMo6ZWhETyfmF7CdKoby5G2Xj9o/QOK5c
gksAGsVaPeht63/+uoizNdKdQCt/cpRyWOLJYsIuwjlFEmDNel2EPIecXmotk48o
2GK2REcCXq+F8W1itywnpmNPlQ==
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions testdata/init/client_intermediate.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC6hDnWJaHuDWMf
Q7JCDGTZuqdFfiP97SPT6ca4a2iWdmttfUhoBkrjrpAmg4z3YHU+ogKyqZtJ7gEh
y+GyntyPEmL8Rua4/5QhW17xzeDqaVbj231azUfeVjbZlqH/ysN+3rVt8rvFlFRs
p0QROqYBClMZNzDBBpSw6hyU0dVNBSMccHGa37xN8jILtq+NI9wVatIRdM/DqTYN
4NmwOdvlJ6cmoyhzfLl1lKVZvxBsj8JVjc/RlK0IvXoGKKQBILk257yEITzkrVT4
swCBbptc+wTtMkLrxYYVF75HqQa/F33gsPP9GnWL9kY48gWI3yHl97gyarQe9DkK
idOsuBO7AgMBAAECggEAGob91thJXJso0uSE8OHkYhcq/TZAljfpFZW1PruAB45W
Hx9ncewbMKC+PcwN+40Lf9n4+kInJO+l15Gwyv/PLMYXr/vBiuRGxCvDqC54474R
rpykCosRwqyOZooHBmNnRJ8WPMn+LM645y8u/ihSPemjAf4YgAuYkETrxbrGVIq9
U/ln+OsgXAS7wOHdz2YuCP2XAoyjbYf7AWUaOnLshQ9CkQqJmUwGZawhK9e0o8jC
7uRNYTIMN91Oog1Ie2iXIfLJ9DODXvgobsp1Mwjbvq6sWssWwmdiZ2/JfNXAopr4
vUEDgU0g8t2Dn6gF3NWIK6K9dSjrP/Pub7it2NXbaQKBgQDfvpJeyqzSIWVMgapf
Kxo64GvCZ/geDp0k880CvlzZ+XpGwFsjtfwlvbcURjtJBl+nEFkxU32Ys+ABj8oO
f02XZp2TI4z3L06njX4NOkkec1tDXjkee3b6i6zusoLlXYW1eBv9hjun6OPkXIlL
dEdlpYnwsfDry4tWUl9DYBUy1wKBgQDVZ7wSzeqlpSxrOKcWQpArDGnfPNiChJaA
oFmqaLogw5ywLTvjkTY16BRO+BSrl13Q2Z6EI9s43Lgo2/3knUmgy964xYb2rcZm
srp1EDcSTn2y2hraoIf6uC9E5TrafAdnx+tRo5fayvctulzD8bqdmKX/UC6gnXo7
k+jTG3RtvQKBgEZkw9JU+7iEz0UZyot0mSUK9HxOj66NNH2qwsZpM/dUWVcnL8V1
fSY2oQIc8owQGEoMh7NQLES92u1C2vRisvu+Sjc/yRr/5EQs7QqmWtjcvEreuyPO
/mjnRvo2aZ5WJTop1syVzfEPAZwYTQ3TQJ9HTLXQlhbpjCYVdmlRMnozAoGBAL9z
mBParpc6ztXGdogO4V3tfhHreWXmY0s0EB1EZS89rpWTjzYCsXq9IXMTNyAV5PGY
OpDbxjGGOqVVb7qorURXghGMDB/EGMlLsOGS0YVX64cICq129Wcl1Cpf6GPYf6BI
h8GvHW4JRqW8mrqjY8M9Djc2DQ7FGMLYWDkEWXiVAoGAds9ZHq2e+5AcedqwFzTd
1B8IivNo0njYk7kWmB0fNtN31Y6UPTP9sYewGCc399AM2FIFb0rYcjqQMTXcjuKW
vLGimUcFVIDjXhImZZuJh1ceukKt2N1QAmM+2YY7GjNy/tZDPV+qn+zvv5rEuUeT
0iXTPSLJ4MXjYCJkqM/a9Jw=
-----END PRIVATE KEY-----
10 changes: 10 additions & 0 deletions testdata/init/intermediate.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[req]
distinguished_name = req_distinguished_name
prompt = no

[req_distinguished_name]
C = US
ST = Nevada
L = Las Vegas
O = github.com/lib/pq
CN = pq Intermediate CA
23 changes: 23 additions & 0 deletions testdata/init/intermediate.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDvTCCAqWgAwIBAgIUWclOnN7wcKaisFrGwvdBCKZaBKIwDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk5ldmFkYTESMBAGA1UEBwwJTGFz
IFZlZ2FzMRowGAYDVQQKDBFnaXRodWIuY29tL2xpYi9wcTEOMAwGA1UEAwwFcHEg
Q0EwHhcNMjYwMzA1MTc0OTA2WhcNMzYwMzA1MTc0OTA2WjBrMQswCQYDVQQGEwJV
UzEPMA0GA1UECAwGTmV2YWRhMRIwEAYDVQQHDAlMYXMgVmVnYXMxGjAYBgNVBAoM
EWdpdGh1Yi5jb20vbGliL3BxMRswGQYDVQQDDBJwcSBJbnRlcm1lZGlhdGUgQ0Ew
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1GmEzB8lZ2c+rraPtuSem
tONNIUHDf2os67jz+ahF7Qv5ypjSkwh1HvTmAwHl+GC+Y60CHI8ALZ9lxnOZUbvo
ZtET1DMayeL32qPgk1P1Bg6nsr+T0gMNUTFipUIULMl1gSTRu4JKAh6+z5UvF5+2
YCa0soDnfAW9xSFXYvaQsY/sn09NXFZSp1CPwTsQcN+Ug5tkVxqiBdSQLRJJjtMV
+Ov4W+JK59p8PNWu4Kby8dFwzLIgnwkP1DmvReMPWrNyIyIWubUJw4PMOkLXVNy6
jQI8oXuapJ0Ib39JUAamn7Ud4qigoAtzNhrIhDrd14nKUbv1BeTDPPnPIMOeA8wN
AgMBAAGjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0G
A1UdDgQWBBSMTYV4MVWf9hD/cI3IJWluimVfXTAfBgNVHSMEGDAWgBREFCFEAXNi
HNiEqOkiEvgsSU/zdzANBgkqhkiG9w0BAQsFAAOCAQEAZl7ok4fyro9SoUxLlHja
wETrRxfkg9lS4xOxj8TcUCAGOpZTGiOTd/bFkoiSnCML0OKyZgbJ+9Dt7NAjgb9p
VeXxn9/Gr4L3U1pBHU+7IjzpZ6NMivdZqL3xu6gryfAxyU3KGA3o0Q3FpZH05YD0
1AcY/2mf3jZghj+yBqa9QNepP6waReIMny6iyJhTerDnovioHjadeWALmhkIqBNx
rmjj9aq3FoJnS9J2FQzoNRigNM86x5HG0EUQHfO/DvR0Ab4qBJ5C9g4uq0LA9CWk
Bafl8W3ViM2kRtZr9NF31DY6EDXe/3WyCNm70ylaOFWePa+9CEYpIeYgSo3kpxNj
8Q==
-----END CERTIFICATE-----
Loading
Loading