From bbd1aeb27223767ad16c6cada7bf51b0fa9e6f2c Mon Sep 17 00:00:00 2001 From: Ben Chobot Date: Thu, 5 Mar 2026 07:10:20 -0800 Subject: [PATCH 01/11] Allow for client certs signed by an intermediate CA. Before this change, client certs signed by an intermediate CA, when the server only trusts the root CA, were not handled correctly. Add a unit test for this situation (which now passes) as well as tests for other intermediate CA situations (which passed before this change and continue to pass afterwards.) Fixes #1266. --- ssl.go | 43 ++++ ssl_intermediate_test.go | 423 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 466 insertions(+) create mode 100644 ssl_intermediate_test.go diff --git a/ssl.go b/ssl.go index caad8015a..2921f232b 100644 --- a/ssl.go +++ b/ssl.go @@ -1,8 +1,10 @@ package pq import ( + "bytes" "crypto/tls" "crypto/x509" + "encoding/pem" "errors" "fmt" "net" @@ -122,6 +124,7 @@ func ssl(cfg Config, mode SSLMode) (func(net.Conn) (net.Conn, error), error) { if err != nil { return nil, err } + sslAppendIntermediates(tlsConf, cfg) // Accept renegotiation requests initiated by the backend. // @@ -223,6 +226,46 @@ func sslClientCertificates(tlsConf *tls.Config, cfg Config) error { return 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) { + if len(tlsConf.Certificates) == 0 || cfg.SSLRootCert == "" { + return + } + + var pemData []byte + if cfg.SSLInline { + pemData = []byte(cfg.SSLRootCert) + } else { + var err error + pemData, err = os.ReadFile(cfg.SSLRootCert) + if err != nil { + return + } + } + + 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) { + tlsConf.Certificates[0].Certificate = append(tlsConf.Certificates[0].Certificate, block.Bytes) + } + } +} + // sslCertificateAuthority adds the RootCA specified in the "sslrootcert" setting. func sslCertificateAuthority(tlsConf *tls.Config, cfg Config) error { // In libpq, the root certificate is only loaded if the setting is not blank. diff --git a/ssl_intermediate_test.go b/ssl_intermediate_test.go new file mode 100644 index 000000000..7469a3c4f --- /dev/null +++ b/ssl_intermediate_test.go @@ -0,0 +1,423 @@ +package pq + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + _ "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "database/sql" + "encoding/pem" + "fmt" + "io" + "math/big" + "net" + "os" + "testing" + "time" + + "github.com/lib/pq/internal/pqtest" +) + +type certChain struct { + rootPEM []byte + intermediatePEM []byte + serverTLSCert tls.Certificate + clientCertPEM []byte + clientKeyPEM []byte +} + +// generateIntermediateCAChain creates: +// - root CA +// - intermediate CA (signed by root) +// - server cert (signed by intermediate) +// - client cert (signed by intermediate) +func generateIntermediateCAChain(t *testing.T) certChain { + t.Helper() + + now := time.Now() + + // Root CA + rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + rootTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "Test Root CA"}, + NotBefore: now, + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + rootCertDER, err := x509.CreateCertificate(rand.Reader, rootTemplate, rootTemplate, &rootKey.PublicKey, rootKey) + if err != nil { + t.Fatal(err) + } + rootCert, err := x509.ParseCertificate(rootCertDER) + if err != nil { + t.Fatal(err) + } + + // Intermediate CA signed by root + interKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + interTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "Test Intermediate CA"}, + NotBefore: now, + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + interCertDER, err := x509.CreateCertificate(rand.Reader, interTemplate, rootCert, &interKey.PublicKey, rootKey) + if err != nil { + t.Fatal(err) + } + interCert, err := x509.ParseCertificate(interCertDER) + if err != nil { + t.Fatal(err) + } + + // Server cert signed by intermediate + serverKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + serverTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(3), + Subject: pkix.Name{CommonName: "localhost"}, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}, + NotBefore: now, + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + serverCertDER, err := x509.CreateCertificate(rand.Reader, serverTemplate, interCert, &serverKey.PublicKey, interKey) + if err != nil { + t.Fatal(err) + } + + // Client cert signed by intermediate + clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + clientTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(4), + Subject: pkix.Name{CommonName: "testclient"}, + NotBefore: now, + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + clientCertDER, err := x509.CreateCertificate(rand.Reader, clientTemplate, interCert, &clientKey.PublicKey, interKey) + if err != nil { + t.Fatal(err) + } + clientKeyDER, err := x509.MarshalECPrivateKey(clientKey) + if err != nil { + t.Fatal(err) + } + + return certChain{ + rootPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCertDER}), + intermediatePEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: interCertDER}), + serverTLSCert: tls.Certificate{ + Certificate: [][]byte{serverCertDER, interCertDER}, + PrivateKey: serverKey, + }, + clientCertPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: clientCertDER}), + clientKeyPEM: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: clientKeyDER}), + } +} + +type mockSSLServerOpts struct { + serverCert tls.Certificate + clientCAs *x509.CertPool // nil means don't request client certs +} + +// mockPostgresSSLServer creates a mock PostgreSQL server with TLS. +// If opts.clientCAs is set, the server requests and manually verifies client +// certificates against that pool (simulating PostgreSQL's ssl_ca_file). +func mockPostgresSSLServer(t *testing.T, opts mockSSLServerOpts) (port string, errCh chan error) { + t.Helper() + + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { l.Close() }) + errCh = make(chan error, 1) + + go func() { + conn, err := l.Accept() + if err != nil { + errCh <- err + return + } + handleMockSSLConn(t, conn, opts, errCh) + }() + + _, port, _ = net.SplitHostPort(l.Addr().String()) + return port, errCh +} + +func handleMockSSLConn(t *testing.T, conn net.Conn, opts mockSSLServerOpts, errCh chan error) { + defer conn.Close() + conn.SetDeadline(time.Now().Add(5 * time.Second)) + + // Read SSL request message + startupMessage := make([]byte, 8) + if _, err := io.ReadFull(conn, startupMessage); err != nil { + errCh <- fmt.Errorf("reading startup: %w", err) + return + } + if !bytes.Equal(startupMessage, []byte{0, 0, 0, 0x8, 0x4, 0xd2, 0x16, 0x2f}) { + errCh <- fmt.Errorf("unexpected startup message: %#v", startupMessage) + return + } + + // Respond with SSLOk + if _, err := conn.Write([]byte("S")); err != nil { + errCh <- fmt.Errorf("writing SSLOk: %w", err) + return + } + + // Configure TLS + tlsCfg := &tls.Config{ + Certificates: []tls.Certificate{opts.serverCert}, + } + if opts.clientCAs != nil { + // RequireAnyClientCert: request a client cert but don't let Go verify + // it automatically. We do manual verification after the handshake to + // simulate what PostgreSQL does. + tlsCfg.ClientAuth = tls.RequireAnyClientCert + } + + tlsConn := tls.Server(conn, tlsCfg) + if err := tlsConn.Handshake(); err != nil { + errCh <- fmt.Errorf("TLS handshake: %w", err) + return + } + defer tlsConn.Close() + + // Manually verify client cert chain if requested. + if opts.clientCAs != nil { + state := tlsConn.ConnectionState() + if len(state.PeerCertificates) == 0 { + errCh <- fmt.Errorf("client did not present a certificate") + return + } + intermediates := x509.NewCertPool() + for _, cert := range state.PeerCertificates[1:] { + intermediates.AddCert(cert) + } + _, err := state.PeerCertificates[0].Verify(x509.VerifyOptions{ + Roots: opts.clientCAs, + Intermediates: intermediates, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + if err != nil { + errCh <- fmt.Errorf("client cert verification failed: %w", err) + return + } + } + + // Read PostgreSQL startup message + buf := make([]byte, 4) + if _, err := io.ReadFull(tlsConn, buf); err != nil { + errCh <- err + return + } + length := int(buf[0])<<24 | int(buf[1])<<16 | int(buf[2])<<8 | int(buf[3]) + if length > 4 { + rest := make([]byte, length-4) + if _, err := io.ReadFull(tlsConn, rest); err != nil { + errCh <- err + return + } + } + + // AuthenticationOk + ReadyForQuery + tlsConn.Write([]byte{'R', 0, 0, 0, 8, 0, 0, 0, 0}) + tlsConn.Write([]byte{'Z', 0, 0, 0, 5, 'I'}) + + // Read client message (Ping sends a simple query) + msgType := make([]byte, 1) + if _, err := io.ReadFull(tlsConn, msgType); err != nil { + errCh <- err + return + } + if _, err := io.ReadFull(tlsConn, buf); err != nil { + errCh <- err + return + } + msgLen := int(buf[0])<<24 | int(buf[1])<<16 | int(buf[2])<<8 | int(buf[3]) + if msgLen > 4 { + body := make([]byte, msgLen-4) + if _, err := io.ReadFull(tlsConn, body); err != nil { + errCh <- err + return + } + } + + // CommandComplete + ReadyForQuery + tlsConn.Write([]byte{'C', 0, 0, 0, 11}) + tlsConn.Write([]byte("SELECT\x00")) + tlsConn.Write([]byte{'Z', 0, 0, 0, 5, 'I'}) + + close(errCh) // success + time.Sleep(100 * time.Millisecond) +} + +func pingMockServer(t *testing.T, dsn string, port string, errCh chan error) error { + t.Helper() + + connector, err := NewConnector(dsn) + if err != nil { + return err + } + db := sql.OpenDB(connector) + defer db.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + clientErr := db.PingContext(ctx) + + // Check server-side error first — it's more informative. + select { + case serverErr, ok := <-errCh: + if ok && serverErr != nil { + return fmt.Errorf("server: %s (client: %v)", serverErr, clientErr) + } + case <-time.After(2 * time.Second): + } + + return clientErr +} + +// TestSSLIntermediateCA tests various intermediate CA scenarios for both +// server certificate verification (verify-ca, verify-full) and client +// certificate authentication. +func TestSSLIntermediateCA(t *testing.T) { + chain := generateIntermediateCAChain(t) + + // Server cert with only the leaf (no intermediate in chain) + serverCertLeafOnly := tls.Certificate{ + Certificate: [][]byte{chain.serverTLSCert.Certificate[0]}, + PrivateKey: chain.serverTLSCert.PrivateKey, + } + + rootCertFile := pqtest.TempFile(t, "root.crt", string(chain.rootPEM)) + bundleCertFile := pqtest.TempFile(t, "bundle.crt", string(chain.rootPEM)+string(chain.intermediatePEM)) + + t.Run("server cert verification", func(t *testing.T) { + tests := []struct { + name string + sslmode string + rootcert string + serverCert tls.Certificate + wantErr bool + }{ + // Server sends full chain [leaf, intermediate], sslrootcert has root only. + { + name: "verify-ca full chain root only", + sslmode: "verify-ca", + rootcert: rootCertFile, + serverCert: chain.serverTLSCert, + }, + { + name: "verify-full full chain root only", + sslmode: "verify-full", + rootcert: rootCertFile, + serverCert: chain.serverTLSCert, + }, + + // Server sends only leaf, sslrootcert has root+intermediate bundle. + { + name: "verify-ca leaf only bundle rootcert", + sslmode: "verify-ca", + rootcert: bundleCertFile, + serverCert: serverCertLeafOnly, + }, + { + name: "verify-full leaf only bundle rootcert", + sslmode: "verify-full", + rootcert: bundleCertFile, + serverCert: serverCertLeafOnly, + }, + + // Server sends only leaf, sslrootcert has root only — can't build chain. + { + name: "verify-ca leaf only root only fails", + sslmode: "verify-ca", + rootcert: rootCertFile, + serverCert: serverCertLeafOnly, + wantErr: true, + }, + { + name: "verify-full leaf only root only fails", + sslmode: "verify-full", + rootcert: rootCertFile, + serverCert: serverCertLeafOnly, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + port, errCh := mockPostgresSSLServer(t, mockSSLServerOpts{ + serverCert: tt.serverCert, + }) + dsn := fmt.Sprintf("host=127.0.0.1 port=%s sslmode=%s sslrootcert=%s user=test dbname=test connect_timeout=5", + port, tt.sslmode, tt.rootcert) + + err := pingMockServer(t, dsn, port, errCh) + if tt.wantErr && err == nil { + t.Fatal("expected error but got nil") + } + if !tt.wantErr && err != nil { + t.Fatalf("expected no error but got: %s", err) + } + }) + } + }) + + t.Run("client cert with intermediate CA", func(t *testing.T) { + // Server's CA trust store has only the root CA. It needs the client to + // send the intermediate cert in its TLS certificate chain. + serverCAs := x509.NewCertPool() + serverCAs.AppendCertsFromPEM(chain.rootPEM) + + clientCertFile := pqtest.TempFile(t, "client.crt", string(chain.clientCertPEM)) + clientKeyFile := pqtest.TempFile(t, "client.key", string(chain.clientKeyPEM)) + if err := os.Chmod(clientKeyFile, 0600); err != nil { + t.Fatal(err) + } + + port, errCh := mockPostgresSSLServer(t, mockSSLServerOpts{ + serverCert: chain.serverTLSCert, + clientCAs: serverCAs, + }) + + dsn := fmt.Sprintf( + "host=127.0.0.1 port=%s sslmode=verify-ca sslrootcert=%s sslcert=%s sslkey=%s user=test dbname=test connect_timeout=5", + port, bundleCertFile, clientCertFile, clientKeyFile) + + err := pingMockServer(t, dsn, port, errCh) + if err != nil { + t.Fatalf("client cert with intermediate CA failed: %s", err) + } + }) +} From 0843cfa37b5443bc70df6b8bd230bfda33927ca6 Mon Sep 17 00:00:00 2001 From: Ben Chobot Date: Thu, 5 Mar 2026 10:09:05 -0800 Subject: [PATCH 02/11] Use pre-generated certs in testdata/init/. Instead of making them inside tests as needed, just have static certs that are made with a Makefile. --- ssl_intermediate_test.go | 210 +++++++------------------- testdata/init/Makefile | 39 ++++- testdata/init/client_intermediate.cnf | 10 ++ testdata/init/client_intermediate.crt | 22 +++ testdata/init/client_intermediate.key | 28 ++++ testdata/init/intermediate.cnf | 10 ++ testdata/init/intermediate.crt | 23 +++ testdata/init/postgresql.crt | 36 ++--- testdata/init/postgresql.key | 52 +++---- testdata/init/root+intermediate.crt | 44 ++++++ testdata/init/root.crt | 41 +++-- testdata/init/server.crt | 41 ++--- testdata/init/server.key | 52 +++---- testdata/init/server_intermediate.cnf | 30 ++++ testdata/init/server_intermediate.crt | 24 +++ testdata/init/server_intermediate.key | 28 ++++ 16 files changed, 422 insertions(+), 268 deletions(-) create mode 100644 testdata/init/client_intermediate.cnf create mode 100644 testdata/init/client_intermediate.crt create mode 100644 testdata/init/client_intermediate.key create mode 100644 testdata/init/intermediate.cnf create mode 100644 testdata/init/intermediate.crt create mode 100644 testdata/init/root+intermediate.crt create mode 100644 testdata/init/server_intermediate.cnf create mode 100644 testdata/init/server_intermediate.crt create mode 100644 testdata/init/server_intermediate.key diff --git a/ssl_intermediate_test.go b/ssl_intermediate_test.go index 7469a3c4f..0751ae468 100644 --- a/ssl_intermediate_test.go +++ b/ssl_intermediate_test.go @@ -3,144 +3,18 @@ package pq import ( "bytes" "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - _ "crypto/sha256" "crypto/tls" "crypto/x509" - "crypto/x509/pkix" "database/sql" "encoding/pem" "fmt" "io" - "math/big" "net" "os" "testing" "time" - - "github.com/lib/pq/internal/pqtest" ) -type certChain struct { - rootPEM []byte - intermediatePEM []byte - serverTLSCert tls.Certificate - clientCertPEM []byte - clientKeyPEM []byte -} - -// generateIntermediateCAChain creates: -// - root CA -// - intermediate CA (signed by root) -// - server cert (signed by intermediate) -// - client cert (signed by intermediate) -func generateIntermediateCAChain(t *testing.T) certChain { - t.Helper() - - now := time.Now() - - // Root CA - rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - rootTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{CommonName: "Test Root CA"}, - NotBefore: now, - NotAfter: now.Add(time.Hour), - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - BasicConstraintsValid: true, - IsCA: true, - } - rootCertDER, err := x509.CreateCertificate(rand.Reader, rootTemplate, rootTemplate, &rootKey.PublicKey, rootKey) - if err != nil { - t.Fatal(err) - } - rootCert, err := x509.ParseCertificate(rootCertDER) - if err != nil { - t.Fatal(err) - } - - // Intermediate CA signed by root - interKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - interTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(2), - Subject: pkix.Name{CommonName: "Test Intermediate CA"}, - NotBefore: now, - NotAfter: now.Add(time.Hour), - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - BasicConstraintsValid: true, - IsCA: true, - } - interCertDER, err := x509.CreateCertificate(rand.Reader, interTemplate, rootCert, &interKey.PublicKey, rootKey) - if err != nil { - t.Fatal(err) - } - interCert, err := x509.ParseCertificate(interCertDER) - if err != nil { - t.Fatal(err) - } - - // Server cert signed by intermediate - serverKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - serverTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(3), - Subject: pkix.Name{CommonName: "localhost"}, - DNSNames: []string{"localhost"}, - IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}, - NotBefore: now, - NotAfter: now.Add(time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - } - serverCertDER, err := x509.CreateCertificate(rand.Reader, serverTemplate, interCert, &serverKey.PublicKey, interKey) - if err != nil { - t.Fatal(err) - } - - // Client cert signed by intermediate - clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - clientTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(4), - Subject: pkix.Name{CommonName: "testclient"}, - NotBefore: now, - NotAfter: now.Add(time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - } - clientCertDER, err := x509.CreateCertificate(rand.Reader, clientTemplate, interCert, &clientKey.PublicKey, interKey) - if err != nil { - t.Fatal(err) - } - clientKeyDER, err := x509.MarshalECPrivateKey(clientKey) - if err != nil { - t.Fatal(err) - } - - return certChain{ - rootPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCertDER}), - intermediatePEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: interCertDER}), - serverTLSCert: tls.Certificate{ - Certificate: [][]byte{serverCertDER, interCertDER}, - PrivateKey: serverKey, - }, - clientCertPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: clientCertDER}), - clientKeyPEM: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: clientKeyDER}), - } -} - type mockSSLServerOpts struct { serverCert tls.Certificate clientCAs *x509.CertPool // nil means don't request client certs @@ -307,20 +181,46 @@ func pingMockServer(t *testing.T, dsn string, port string, errCh chan error) err return clientErr } +// loadServerCert loads a TLS server certificate, optionally including the +// intermediate CA cert in the chain. +func loadServerCert(t *testing.T, certFile, keyFile, intermediateFile string) tls.Certificate { + t.Helper() + + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + t.Fatal(err) + } + if intermediateFile != "" { + interPEM, err := os.ReadFile(intermediateFile) + if err != nil { + t.Fatal(err) + } + block, _ := pem.Decode(interPEM) + if block != nil && block.Type == "CERTIFICATE" { + cert.Certificate = append(cert.Certificate, block.Bytes) + } + } + return cert +} + // TestSSLIntermediateCA tests various intermediate CA scenarios for both // server certificate verification (verify-ca, verify-full) and client // certificate authentication. func TestSSLIntermediateCA(t *testing.T) { - chain := generateIntermediateCAChain(t) - + const ( + rootCert = "testdata/init/root.crt" + bundleCert = "testdata/init/root+intermediate.crt" + interCert = "testdata/init/intermediate.crt" + serverCert = "testdata/init/server_intermediate.crt" + serverKey = "testdata/init/server_intermediate.key" + clientCert = "testdata/init/client_intermediate.crt" + clientKey = "testdata/init/client_intermediate.key" + ) + + // Server cert with full chain [leaf, intermediate] + serverFullChain := loadServerCert(t, serverCert, serverKey, interCert) // Server cert with only the leaf (no intermediate in chain) - serverCertLeafOnly := tls.Certificate{ - Certificate: [][]byte{chain.serverTLSCert.Certificate[0]}, - PrivateKey: chain.serverTLSCert.PrivateKey, - } - - rootCertFile := pqtest.TempFile(t, "root.crt", string(chain.rootPEM)) - bundleCertFile := pqtest.TempFile(t, "bundle.crt", string(chain.rootPEM)+string(chain.intermediatePEM)) + serverLeafOnly := loadServerCert(t, serverCert, serverKey, "") t.Run("server cert verification", func(t *testing.T) { tests := []struct { @@ -334,43 +234,43 @@ func TestSSLIntermediateCA(t *testing.T) { { name: "verify-ca full chain root only", sslmode: "verify-ca", - rootcert: rootCertFile, - serverCert: chain.serverTLSCert, + rootcert: rootCert, + serverCert: serverFullChain, }, { name: "verify-full full chain root only", sslmode: "verify-full", - rootcert: rootCertFile, - serverCert: chain.serverTLSCert, + rootcert: rootCert, + serverCert: serverFullChain, }, // Server sends only leaf, sslrootcert has root+intermediate bundle. { name: "verify-ca leaf only bundle rootcert", sslmode: "verify-ca", - rootcert: bundleCertFile, - serverCert: serverCertLeafOnly, + rootcert: bundleCert, + serverCert: serverLeafOnly, }, { name: "verify-full leaf only bundle rootcert", sslmode: "verify-full", - rootcert: bundleCertFile, - serverCert: serverCertLeafOnly, + rootcert: bundleCert, + serverCert: serverLeafOnly, }, // Server sends only leaf, sslrootcert has root only — can't build chain. { name: "verify-ca leaf only root only fails", sslmode: "verify-ca", - rootcert: rootCertFile, - serverCert: serverCertLeafOnly, + rootcert: rootCert, + serverCert: serverLeafOnly, wantErr: true, }, { name: "verify-full leaf only root only fails", sslmode: "verify-full", - rootcert: rootCertFile, - serverCert: serverCertLeafOnly, + rootcert: rootCert, + serverCert: serverLeafOnly, wantErr: true, }, } @@ -397,25 +297,27 @@ func TestSSLIntermediateCA(t *testing.T) { t.Run("client cert with intermediate CA", func(t *testing.T) { // Server's CA trust store has only the root CA. It needs the client to // send the intermediate cert in its TLS certificate chain. + rootPEM, err := os.ReadFile(rootCert) + if err != nil { + t.Fatal(err) + } serverCAs := x509.NewCertPool() - serverCAs.AppendCertsFromPEM(chain.rootPEM) + serverCAs.AppendCertsFromPEM(rootPEM) - clientCertFile := pqtest.TempFile(t, "client.crt", string(chain.clientCertPEM)) - clientKeyFile := pqtest.TempFile(t, "client.key", string(chain.clientKeyPEM)) - if err := os.Chmod(clientKeyFile, 0600); err != nil { + if err := os.Chmod(clientKey, 0600); err != nil { t.Fatal(err) } port, errCh := mockPostgresSSLServer(t, mockSSLServerOpts{ - serverCert: chain.serverTLSCert, + serverCert: serverFullChain, clientCAs: serverCAs, }) dsn := fmt.Sprintf( "host=127.0.0.1 port=%s sslmode=verify-ca sslrootcert=%s sslcert=%s sslkey=%s user=test dbname=test connect_timeout=5", - port, bundleCertFile, clientCertFile, clientKeyFile) + port, bundleCert, clientCert, clientKey) - err := pingMockServer(t, dsn, port, errCh) + err = pingMockServer(t, dsn, port, errCh) if err != nil { t.Fatalf("client cert with intermediate CA failed: %s", err) } diff --git a/testdata/init/Makefile b/testdata/init/Makefile index a84e31e9c..a62fd9b3d 100644 --- a/testdata/init/Makefile +++ b/testdata/init/Makefile @@ -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 \ @@ -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 diff --git a/testdata/init/client_intermediate.cnf b/testdata/init/client_intermediate.cnf new file mode 100644 index 000000000..fa8ffc489 --- /dev/null +++ b/testdata/init/client_intermediate.cnf @@ -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 diff --git a/testdata/init/client_intermediate.crt b/testdata/init/client_intermediate.crt new file mode 100644 index 000000000..453b75a5e --- /dev/null +++ b/testdata/init/client_intermediate.crt @@ -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----- diff --git a/testdata/init/client_intermediate.key b/testdata/init/client_intermediate.key new file mode 100644 index 000000000..4ed44d1a9 --- /dev/null +++ b/testdata/init/client_intermediate.key @@ -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----- diff --git a/testdata/init/intermediate.cnf b/testdata/init/intermediate.cnf new file mode 100644 index 000000000..12513f1a9 --- /dev/null +++ b/testdata/init/intermediate.cnf @@ -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 diff --git a/testdata/init/intermediate.crt b/testdata/init/intermediate.crt new file mode 100644 index 000000000..d1648f128 --- /dev/null +++ b/testdata/init/intermediate.crt @@ -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----- diff --git a/testdata/init/postgresql.crt b/testdata/init/postgresql.crt index c1815f865..20c425f4e 100644 --- a/testdata/init/postgresql.crt +++ b/testdata/init/postgresql.crt @@ -1,20 +1,22 @@ -----BEGIN CERTIFICATE----- -MIIDPjCCAiYCCQD4nsC6zsmIqjANBgkqhkiG9w0BAQsFADBeMQswCQYDVQQGEwJV +MIIDkjCCAnqgAwIBAgIUWclOnN7wcKaisFrGwvdBCKZaBKEwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk5ldmFkYTESMBAGA1UEBwwJTGFz +IFZlZ2FzMRowGAYDVQQKDBFnaXRodWIuY29tL2xpYi9wcTEOMAwGA1UEAwwFcHEg +Q0EwHhcNMjYwMzA1MTc0OTA2WhcNMzYwMzA1MTc0OTA2WjBkMQswCQYDVQQGEwJV UzEPMA0GA1UECAwGTmV2YWRhMRIwEAYDVQQHDAlMYXMgVmVnYXMxGjAYBgNVBAoM -EWdpdGh1Yi5jb20vbGliL3BxMQ4wDAYDVQQDDAVwcSBDQTAeFw0yMTA5MDIwMTU1 -MDJaFw0zMTA5MDMwMTU1MDJaMGQxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIDAZOZXZh -ZGExEjAQBgNVBAcMCUxhcyBWZWdhczEaMBgGA1UECgwRZ2l0aHViLmNvbS9saWIv -cHExFDASBgNVBAMMC3BxZ29zc2xjZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAx0ucPVUNCrVmbyithwWrmmZ1dGudBwhSyDB6af4z5Cr+S6dx2SRU -UGUw3Lv+z+tUqQ7hJj0oNddIQeYKl/Tt6JPpZsQfERP/cUGedtyt7HnCKobBL+0B -NvHnDIUiIL4LgfiZK4DWJkGmm7nTHo/7qKAw60vCMLUW98DC0Xhlk9MHYG+e9Zai -3G0vY2X6DUYcSmzBI3JakFEgMZTQg3ofUQMz8TYeK3/DYadLXkl08d18LL3Dnefx -0xRuBPNTa2tLfVnFkfFi6Z9xVB/WhG6+X4OLnO85v5xUOGTV+g154iR7FOkrrl5F -lEUBj+yaIoTRi+MyZ/oYqWwQUDYS3+Te9wIDAQABMA0GCSqGSIb3DQEBCwUAA4IB -AQCCJpwUWCx7xfXv3vH3LQcffZycyRHYPgTCbiQw3x9aBb77jUAh5O6lEj/W0nx2 -SCTEsCsRSAiFwfUb+g/AFCW84dELRWmf38eoqACebLymqnvxyZA+O87yu07XyFZR -TnmbDMzZgsyWWGwS3JoGFk+ibWY4AImYQnSJO8Pi0kZ37ngbAyJ3RtDhhEQJWw/Q -D04p3uky/ea7Gyz0QTx5o40n4gq7nEzF1OS6IHozM840J5aZrxRiXEa56fsmJHmI -IGyI07SGlWJ15r1wc8lB+8ilnAqH1QQlYzTIW0Q4NZE7n3uQg1EVuueGiGO2ex2/ -he9lDiJfOQuPuLbOxzctP9v9 +EWdpdGh1Yi5jb20vbGliL3BxMRQwEgYDVQQDDAtwcWdvc3NsY2VydDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBANRQzm2XI6vUZ52ZAfcz5iZnNOzSrx7q +gwASyI4zzWKFlei22wiqsjxmzZAzD9cv5P1ieavK3vfcovIrjs3TkIhHNFs2+ssV +fgNgu3WtwVnpZMdmflbBcO4JizaHFXYw/E6I8OmxsuxJLxAaMSeBUaBF8rmzpOvr +dVgiyZIR4KpB0Bft7lgyJfXGi08GnIQlbR0IyVNk9TTEIfJt0vPvvMYh9s94n7vM +S/oL7473Hbkui6wQMGALeKgdjhmp3ktPmcmysHNJhlPh16qZoX7+saSQFtNU2CYD +r4d+4mZ5V2wKS4e4ia1tXVlfPIL8yBctAG17TUvZJJ9L3x8T/H8BPvkCAwEAAaNC +MEAwHQYDVR0OBBYEFIYjRJ883GGJ1zdqgeQNZsVEHWEZMB8GA1UdIwQYMBaAFEQU +IUQBc2Ic2ISo6SIS+CxJT/N3MA0GCSqGSIb3DQEBCwUAA4IBAQASBN2x0M4REx9K +ULTwHdJbTtHcTeVnuDmzOd+qOb/vmMl/OqO8XDeiv01AxXwOKP9aX9//6BrbYl5W +PTg4kTOhemhAp+20ZUlgss0CDOkxk7RT2T2SajlNXNP9iHLs9KvvFwdW48fgZard +m3PStQiwjjsKJ7hCuLxGAqBF0YrlIdI5cRUHRX9pz8r12Eb4qX+lCfQciDKa68jx +vxLmJsu/Ffn5gtUZFsmvWJ+dEh5yDN4tbh+eR0GFVM/VHzzCbHI9WbXFPvDljLal +WYZPc6b3HakvvYcefE+UKQjEVctcRo29m+ibq7Ph6ubOYk8/FEjDKLjOnFiNj5Tq +saDI0kkw -----END CERTIFICATE----- diff --git a/testdata/init/postgresql.key b/testdata/init/postgresql.key index 8380da4bc..b76bd93e0 100644 --- a/testdata/init/postgresql.key +++ b/testdata/init/postgresql.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDHS5w9VQ0KtWZv -KK2HBauaZnV0a50HCFLIMHpp/jPkKv5Lp3HZJFRQZTDcu/7P61SpDuEmPSg110hB -5gqX9O3ok+lmxB8RE/9xQZ523K3secIqhsEv7QE28ecMhSIgvguB+JkrgNYmQaab -udMej/uooDDrS8IwtRb3wMLReGWT0wdgb571lqLcbS9jZfoNRhxKbMEjclqQUSAx -lNCDeh9RAzPxNh4rf8Nhp0teSXTx3XwsvcOd5/HTFG4E81Nra0t9WcWR8WLpn3FU -H9aEbr5fg4uc7zm/nFQ4ZNX6DXniJHsU6SuuXkWURQGP7JoihNGL4zJn+hipbBBQ -NhLf5N73AgMBAAECggEAHLNY1sRO0oH5NHzpMI6yfdPPimqM/JxIP6grmOQQ2QUQ -BhkhHiJLOiC4frFcKtk7IfWQmw8noUlVkJfuYp/VOy9B55jK2IzGtqq6hWeWbH3E -Zpdtbtd021LO8VCi75Au3BLPDCLLtEq0Ea0bKEWX+lrHcLtCRf1uR1OtOrlZ94Wl -DUhm7YJC4cS1bi6Kdf03R+fw2oFi7/QdywcT4ow032jGWOly/Jl7bSHZK7xLtM/i -9HfMwmusD/iuz7mtLU7VCpnlKZm6MfS5D427ybW8MruuiZEtQJ6QtRIrHBHk93aK -Op0tjJ6tMav1UsJzgVz9+uWILE9l0AjAa4AvbfNzEQKBgQD8mma9SLQPtBb6cXuT -CQgjE4vyph8mRnm/pTz3QLIpMiLy2+aKJD/u4cduzLw1vjuH1tlb7NQ9c891jAJh -JhwDwqKAXfFicfRs/PYWngx/XtGhbbpgm1yA6XuYL1D06gzmjzXgHvZMOFcts+GF -y0JEuV7v6eYrpQJRQYCwY6xTgwKBgQDJ+bHAlgOaC94DZEXZMiUznCCjBjAstiXG -BEN7Cnfn6vgvPm/b6BkKn4VrsCmbZQKT7QJDSOhYwXCC2ZlrKiF8GEUHX4mi8347 -8B+DsuokTLNmN61QAZbb1c3XQVnr15xH8ijm7yYs4tCBmVLKBmpw1T4IZXXlVE5k -gmee+AwIfQKBgGr+P0wnclVAc4cq8CusZKzux5VEtebxbPo21CbqWUxHtzPk3rZe -elIFggK1Z3bgF7kG0NQ18QQCfLoOTqe1i6IwG8KBiA+pst1DHD0iPqroj6RvpMTs -qXbU7ovcZs8GH+a8fBZtJufL6WkrSvfvyybu2X6HNP4Bi4S9WPPdlA1fAoGAE5m/ -vkjQoKp2KS4Z+TH8mj2UjT2Uf0JN+CGByvcBG+iZnTwZ7uVfSMCiWgkGgKYU0fY2 -OgFhSvu6x3gGg3fbOAfC6yxCVyX6IibzZ/x87HjlEA5nK1R8J2lgSHt3FoQeDn1Z -qs+ajNCWG32doy1sNvb6xiXSgybjVK2zEKJRyKECgYBJTk2IABebjvInNb6tagcI -nD4d2LgBmZJZsTruHXrpO0s3XCQcFKks4JKH1CVjd34f7LkxzEOGbE7wKBBd652s -ob6gFKnbqTniTo3NRUycB6ymo4LSaBvKgeY5hYbVxrYheRLPGY+gPVYb3VMKu9N9 -76rcaFqJOz7OeywRG5bHUg== +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDUUM5tlyOr1Ged +mQH3M+YmZzTs0q8e6oMAEsiOM81ihZXottsIqrI8Zs2QMw/XL+T9Ynmryt733KLy +K47N05CIRzRbNvrLFX4DYLt1rcFZ6WTHZn5WwXDuCYs2hxV2MPxOiPDpsbLsSS8Q +GjEngVGgRfK5s6Tr63VYIsmSEeCqQdAX7e5YMiX1xotPBpyEJW0dCMlTZPU0xCHy +bdLz77zGIfbPeJ+7zEv6C++O9x25LousEDBgC3ioHY4Zqd5LT5nJsrBzSYZT4deq +maF+/rGkkBbTVNgmA6+HfuJmeVdsCkuHuImtbV1ZXzyC/MgXLQBte01L2SSfS98f +E/x/AT75AgMBAAECggEADy8kREQt4ekT6/p4YISOriptZ459xblB2yx8uWbNBoHF +Qdpp+cmza4xyoSB1vo8HUnPLfdTJc3KNGMKyuNerm4N0JB3fe8yysW0mmvjtPg/q +DDSxTmURPYTjNugcSdKhCMDUcfIqeKmXOxCUeV3PR6YZANzvK4wwXThHsDGlHqfq +701Ia1tfXmaxsW3vh3Q26GM+rWYY2kowvf/H49fCIeBJJRsUDZMD4Xpsxxs/IS1o +vjpKFzvRSfa5M78o00D+W1xgiTOr3aq3/lahQTF4jY+lZqiQ0AZ0aaw9fQ/Qmg1l +wmZT3GNa7jK13Bx6MnPg1eDnoxpsv/vWhO4+oiW2nQKBgQDseWTpPDE4yYwiLmqx +6q+8UW/3EdGzsup1+7mIHko+sFj1rzUH97m4EVaVEJY6O2N1LMmx+ygiNobAnKD+ +DVFyaGiuVcVkFH5dzA+mZ4mQH68RiyGdJbos0EkyyzBGceOBsenUJb8eGQrOLe5x +f8JHfAdMhawnMAVgjbFGLD6xAwKBgQDl2L9TneY7kEl6+ykb9DI3WBGwoVW9sKgT +ZusG2rDMz6+a94GFtLiKYNqW+5By8+e2cSCSFUIgw9hIMSYe3iVkQtN9heHNS91L +mo8HLBBfWKBogiciR+7RoiVKmWlrUxJRB1egBoyVku8AAuPv203sInXw7yMAsINe +BwxUGLxJUwKBgDsrDLLHGtHIrEWJM60cuaf3AHrjfILoC35F4+BJI+6XlUqz8iLy +OwfxXqahpdUgMvyInlboLtyQpBrhle67SlvEAB1O3Lrz3cJ+YpQSZ/sl0lojY9+8 +Jt87pnUNHiEiMfirmYQ5hZ50SZ3ZJEZF30ifofvlnnOXsmC8U1TpkS4dAoGAd/8T +zCzIcVpf8nPDv5Na0CgLfEKOh+z9ort9gmOUIClhja8gENUL7zqnhGGrxUfvNgGT +tpCgqIh2lyIJu93QeaqlzYejHlI4I2t6tozbs1uKy2T/11bkqM9VVsAHGIATNPh3 +V9VzefdvDXt00tmUse4/0tyWUprUyrc1SYZsbn8CgYBGRc/oryM/dq4b+YuW4c3r +5OrrcriElY87V8CxynBvGPkHRLJw1frdcmftfZB/5X+7jYiJ/ebE3xm2gBnIfdF9 +RVBi3FGG6CLUbkjWHVu2IUc7Vkgtwvhs1VF4W04hn9kZckFUEvgldQmIUHuRJLMk +w4VGBfPlGwxE/8H5zNFfFw== -----END PRIVATE KEY----- diff --git a/testdata/init/root+intermediate.crt b/testdata/init/root+intermediate.crt new file mode 100644 index 000000000..d286c62af --- /dev/null +++ b/testdata/init/root+intermediate.crt @@ -0,0 +1,44 @@ +-----BEGIN CERTIFICATE----- +MIIDjDCCAnSgAwIBAgIUeR5juWeV3BIT+Ctow8tPaWuOsvwwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk5ldmFkYTESMBAGA1UEBwwJTGFz +IFZlZ2FzMRowGAYDVQQKDBFnaXRodWIuY29tL2xpYi9wcTEOMAwGA1UEAwwFcHEg +Q0EwHhcNMjYwMzA1MTc0NjM1WhcNMzYwMzA1MTc0NjM1WjBeMQswCQYDVQQGEwJV +UzEPMA0GA1UECAwGTmV2YWRhMRIwEAYDVQQHDAlMYXMgVmVnYXMxGjAYBgNVBAoM +EWdpdGh1Yi5jb20vbGliL3BxMQ4wDAYDVQQDDAVwcSBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAI81fnu8lXFr/jUvM142WGkfEPi3Jwb49euQNr/n +lyuLVpXDyN0ki3ymxHeXBP2JHLufoEJbwf1OMYSIZ6sEQPivBghYrmTR6YvJHrEC +APxrvTtPzQc/9++c9m9gri5+4tPCkMTYCvhszp7G9kYJIuHlutWZ7WCm8XPh0n6M ++0j86xpJdW3WWyLCiku61HHtCX38YaCV2tkWArQ78K/CZT60zYooA9rH8+WlwCV9 +CKrubQlnvaR0HuSmMH8AFYNyBqdMDL7RzyvWTMCTOnjDnRkx0QVeh6kNHBCUaUJB +xSLILWxFx/rEQYfWK4D2ckxfsO7qQLLq4imXP2ejbig3o4UCAwEAAaNCMEAwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFEQUIUQBc2Ic +2ISo6SIS+CxJT/N3MA0GCSqGSIb3DQEBCwUAA4IBAQAdAuAEWTIUsspCOODGSJ2g +7AHzzfZsttTD97WYK/u50NmRdaHCTpA0hhjWWPPbseaseaAvniMR+V2h2pbOX+rA +cALOMjW7iAw0uSHpY027G+W8SewxNptjgY0CEMzo+QpgbCaljR2jy06fu8VNY1FA ++B1Fzb7yUDSznC1zDVvwD8Nev8GqJ9nlQ8DxC8Sc8H+LL3xxy+yzEPL5Fq9JR9Dh +1W6gEFsRfE2iOI2nAiPNoxuVFUslf0FYM8MUW3OH/2u6jLz1t521Q6bdHlMoVwab +BONVbhZEKOKuvcbv1cNn6Qv3KxbkOTIqhZKLQ6Ts3f3SKgm6O+sItZqKsKzrTAhd +-----END CERTIFICATE----- +-----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----- diff --git a/testdata/init/root.crt b/testdata/init/root.crt index 390a907c3..f8d92b673 100644 --- a/testdata/init/root.crt +++ b/testdata/init/root.crt @@ -1,24 +1,21 @@ -----BEGIN CERTIFICATE----- -MIIEBjCCAu6gAwIBAgIJAPizR+OD14YnMA0GCSqGSIb3DQEBCwUAMF4xCzAJBgNV -BAYTAlVTMQ8wDQYDVQQIDAZOZXZhZGExEjAQBgNVBAcMCUxhcyBWZWdhczEaMBgG -A1UECgwRZ2l0aHViLmNvbS9saWIvcHExDjAMBgNVBAMMBXBxIENBMB4XDTIxMDkw -MjAxNTUwMloXDTMxMDkwMzAxNTUwMlowXjELMAkGA1UEBhMCVVMxDzANBgNVBAgM -Bk5ldmFkYTESMBAGA1UEBwwJTGFzIFZlZ2FzMRowGAYDVQQKDBFnaXRodWIuY29t -L2xpYi9wcTEOMAwGA1UEAwwFcHEgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQDb9d6sjdU6GdibGrXRMOHREH3MRUS8T4TFqGgPEGVDP/V5bAZlBSGP -AN0o9DTyVLcbQpBt8zMTw9KeIzIIe5NIVkSmA16lw/YckGhOM+kZIkiDuE6qt5Ia -OQCRMdXkZ8ejG/JUu+rHU8FJZL8DE+jyYherzdjkeVAQ7JfzxAwW2Dl7T/47g337 -Pwmf17AEb8ibSqmXyUN7R5NhJQs+hvaYdNagzdx91E1H+qlyBvmiNeasUQljLvZ+ -Y8wAuU79neA+d09O4PBiYwV17rSP6SZCeGE3oLZviL/0KM9Xig88oB+2FmvQ6Zxa -L7SoBlqS+5pBZwpH7eee/wCIKAnJtMAJAgMBAAGjgcYwgcMwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUfIXEczahbcM2cFrwclJF7GbdajkwgZAGA1UdIwSBiDCB -hYAUfIXEczahbcM2cFrwclJF7GbdajmhYqRgMF4xCzAJBgNVBAYTAlVTMQ8wDQYD -VQQIDAZOZXZhZGExEjAQBgNVBAcMCUxhcyBWZWdhczEaMBgGA1UECgwRZ2l0aHVi -LmNvbS9saWIvcHExDjAMBgNVBAMMBXBxIENBggkA+LNH44PXhicwDQYJKoZIhvcN -AQELBQADggEBABFyGgSz2mHVJqYgX1Y+7P+MfKt83cV2uYDGYvXrLG2OGiCilVul -oTBG+8omIMSHOsQZvWMpA5H0tnnlQHrKpKpUyKkSL+Wv5GL0UtBmHX7mVRiaK2l4 -q2BjRaQUitp/FH4NSdXtVrMME5T1JBBZHsQkNL3cNRzRKwY/Vj5UGEDxDS7lILUC -e01L4oaK0iKQn4beALU+TvKoAHdPvoxpPpnhkF5ss9HmdcvRktJrKZemDJZswZ7/ -+omx8ZPIYYUH5VJJYYE88S7guAt+ZaKIUlel/t6xPbo2ZySFSg9u1uB99n+jTo3L -1rAxFnN3FCX2jBqgP29xMVmisaN5k04UmyI= +MIIDjDCCAnSgAwIBAgIUeR5juWeV3BIT+Ctow8tPaWuOsvwwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk5ldmFkYTESMBAGA1UEBwwJTGFz +IFZlZ2FzMRowGAYDVQQKDBFnaXRodWIuY29tL2xpYi9wcTEOMAwGA1UEAwwFcHEg +Q0EwHhcNMjYwMzA1MTc0NjM1WhcNMzYwMzA1MTc0NjM1WjBeMQswCQYDVQQGEwJV +UzEPMA0GA1UECAwGTmV2YWRhMRIwEAYDVQQHDAlMYXMgVmVnYXMxGjAYBgNVBAoM +EWdpdGh1Yi5jb20vbGliL3BxMQ4wDAYDVQQDDAVwcSBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAI81fnu8lXFr/jUvM142WGkfEPi3Jwb49euQNr/n +lyuLVpXDyN0ki3ymxHeXBP2JHLufoEJbwf1OMYSIZ6sEQPivBghYrmTR6YvJHrEC +APxrvTtPzQc/9++c9m9gri5+4tPCkMTYCvhszp7G9kYJIuHlutWZ7WCm8XPh0n6M ++0j86xpJdW3WWyLCiku61HHtCX38YaCV2tkWArQ78K/CZT60zYooA9rH8+WlwCV9 +CKrubQlnvaR0HuSmMH8AFYNyBqdMDL7RzyvWTMCTOnjDnRkx0QVeh6kNHBCUaUJB +xSLILWxFx/rEQYfWK4D2ckxfsO7qQLLq4imXP2ejbig3o4UCAwEAAaNCMEAwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFEQUIUQBc2Ic +2ISo6SIS+CxJT/N3MA0GCSqGSIb3DQEBCwUAA4IBAQAdAuAEWTIUsspCOODGSJ2g +7AHzzfZsttTD97WYK/u50NmRdaHCTpA0hhjWWPPbseaseaAvniMR+V2h2pbOX+rA +cALOMjW7iAw0uSHpY027G+W8SewxNptjgY0CEMzo+QpgbCaljR2jy06fu8VNY1FA ++B1Fzb7yUDSznC1zDVvwD8Nev8GqJ9nlQ8DxC8Sc8H+LL3xxy+yzEPL5Fq9JR9Dh +1W6gEFsRfE2iOI2nAiPNoxuVFUslf0FYM8MUW3OH/2u6jLz1t521Q6bdHlMoVwab +BONVbhZEKOKuvcbv1cNn6Qv3KxbkOTIqhZKLQ6Ts3f3SKgm6O+sItZqKsKzrTAhd -----END CERTIFICATE----- diff --git a/testdata/init/server.crt b/testdata/init/server.crt index 1a0ac0d43..e588accd9 100644 --- a/testdata/init/server.crt +++ b/testdata/init/server.crt @@ -1,22 +1,23 @@ -----BEGIN CERTIFICATE----- -MIIDqzCCApOgAwIBAgIJAPiewLrOyYipMA0GCSqGSIb3DQEBCwUAMF4xCzAJBgNV -BAYTAlVTMQ8wDQYDVQQIDAZOZXZhZGExEjAQBgNVBAcMCUxhcyBWZWdhczEaMBgG -A1UECgwRZ2l0aHViLmNvbS9saWIvcHExDjAMBgNVBAMMBXBxIENBMB4XDTIxMDkw -MjAxNTUwMloXDTMxMDkwMzAxNTUwMlowTjELMAkGA1UEBhMCVVMxDzANBgNVBAgM -Bk5ldmFkYTESMBAGA1UEBwwJTGFzIFZlZ2FzMRowGAYDVQQKDBFnaXRodWIuY29t -L2xpYi9wcTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKf6H4UzmANN -QiQJe92Mf3ETMYmpZKNNO9DPEHyNLIkag+XwMrBTdcCK0mLvsNCYpXuBN6703KCd -WAFOeMmj7gOsWtvjt5Xm6bRHLgegekXzcG/jDwq/wyzeDzr/YkITuIlG44Lf9lhY -FLwiHlHOWHnwrZaEh6aU//02aQkzyX5INeXl/3TZm2G2eIH6AOxOKOU27MUsyVSQ -5DE+SDKGcRP4bElueeQWvxAXNMZYb7sVSDdfHI3zr32K4k/tC8x0fZJ5XN/dvl4t -4N4MrYlmDO5XOrb/gQH1H4iu6+5EMDfZYab4fkThnNFdfFqu4/8Scv7KZ8mWqpKM -fGAjEPctQi0CAwEAAaN8MHowHQYDVR0OBBYEFENExPbmDyFB2AJUdbMvVyhlNPD5 -MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMBMGA1UdEQQMMAqCCHBvc3RncmVzMCwG -CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTANBgkq -hkiG9w0BAQsFAAOCAQEAMRVbV8RiEsmp9HAtnVCZmRXMIbgPGrqjeSwk586s4K8v -BSqNCqxv6s5GfCRmDYiqSqeuCVDtUJS1HsTmbxVV7Ke71WMo+xHR1ICGKOa8WGCb -TGsuicG5QZXWaxeMOg4s0qpKmKko0d1aErdVsanU5dkrVS7D6729Ffnzu4lwApk6 -invAB67p8u7sojwqRq5ce0vRaG+YFylTrWomF9kauEb8gKbQ9Xc7QfX+h+UH/mq9 -Nvdj8LOHp6/82bZdnsYUOtV4lS1IA/qzeXpqBphxqfWabD1yLtkyJyImZKq8uIPp -0CG4jhObPdWcCkXD6bg3QK3mhwlC79OtFgxWmldCRQ== +MIID2TCCAsGgAwIBAgIUWclOnN7wcKaisFrGwvdBCKZaBKAwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk5ldmFkYTESMBAGA1UEBwwJTGFz +IFZlZ2FzMRowGAYDVQQKDBFnaXRodWIuY29tL2xpYi9wcTEOMAwGA1UEAwwFcHEg +Q0EwHhcNMjYwMzA1MTc0OTA2WhcNMzYwMzA1MTc0OTA2WjBOMQswCQYDVQQGEwJV +UzEPMA0GA1UECAwGTmV2YWRhMRIwEAYDVQQHDAlMYXMgVmVnYXMxGjAYBgNVBAoM +EWdpdGh1Yi5jb20vbGliL3BxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA2bkHjKpFAhgd7IdJVyviBjyVnxHRZ9sGDfFyjf/wFdg17YZKPDxTCs2deg7s +Raq0X0Rk7uzl9XECYAJbfmaJw0XE6ZZPTS098oXcIXp+jHgul31lDlvuRAxWIFUM +Yb5FHnR3KADx7JnnuXh2ARxl8+dpgaLRqwx/gAkoBKvBd/IEqOphcR5vbcXsN89C +mrcMOsmWltgPrMZRQBNGe9j1P6HZ3NP4kWPfCzhWqs3rEZUX0TSKro+Jx9V8yNsB +nHfF7KjLQa3fFlAu5iQTo4Dn2SD/8fM1D+Lz4x0y7QDoETcb0OE2r2V2T7h/nkIE +yyl6D74IwXUsLNMF2koaTyh9yQIDAQABo4GeMIGbMB0GA1UdDgQWBBQTm+oKqIy2 +7XhUSB4ekGcpJKgvvDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDATBgNVHREEDDAK +gghwb3N0Z3JlczAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2Vy +dGlmaWNhdGUwHwYDVR0jBBgwFoAURBQhRAFzYhzYhKjpIhL4LElP83cwDQYJKoZI +hvcNAQELBQADggEBABhbdQ2Geq3zQuo1xV79PO/E62LQoX9MWjyI2Ue7pWmEEppX +/yPLACrusmqdEuWmeY6Vn3fcFvHwWRkBVoBFkIQfbKSSexT6VkHEOl+MMLDpi1yp +KFGfw46esPasg7IPDJ52WGNcfAvYrUWtFohKjkkQoknImE4JlaYcQUqSDqBN8ics +gUcyw8V/yy/lqE4lEfAbdFOu/rxv9auYktmuScYEvvkzn4cpV29X+XspjWGc85kI +TuaqIpP2vW79BEk7pgbQQ+kpN9uN7wq5eoNXLDSSCNMQk7gmGDYq7Ww/4j65nBWf +FCONlMQE36idZfA+mneMmKrXJ2dQ+f4N8gzmPnU= -----END CERTIFICATE----- diff --git a/testdata/init/server.key b/testdata/init/server.key index 152e0f23e..c6c7c8c2c 100644 --- a/testdata/init/server.key +++ b/testdata/init/server.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCn+h+FM5gDTUIk -CXvdjH9xEzGJqWSjTTvQzxB8jSyJGoPl8DKwU3XAitJi77DQmKV7gTeu9NygnVgB -TnjJo+4DrFrb47eV5um0Ry4HoHpF83Bv4w8Kv8Ms3g86/2JCE7iJRuOC3/ZYWBS8 -Ih5Rzlh58K2WhIemlP/9NmkJM8l+SDXl5f902ZthtniB+gDsTijlNuzFLMlUkOQx -PkgyhnET+GxJbnnkFr8QFzTGWG+7FUg3XxyN8699iuJP7QvMdH2SeVzf3b5eLeDe -DK2JZgzuVzq2/4EB9R+IruvuRDA32WGm+H5E4ZzRXXxaruP/EnL+ymfJlqqSjHxg -IxD3LUItAgMBAAECggEAOE2naQ9tIZYw2EFxikZApVcooJrtx6ropMnzHbx4NBB2 -K4mChAXFj184u77ZxmGT/jzGvFcI6LE0wWNbK0NOUV7hKZk/fPhkV3AQZrAMrAu4 -IVi7PwAd3JkmA8F8XuebUDA5rDGDsgL8GD9baFJA58abeLs9eMGyuF4XgOUh4bip -hgHa76O2rcDWNY5HZqqRslw75FzlYkB0PCts/UJxSswj70kTTihyOhDlrm2TnyxI -ne54UbGRrpfs9wiheSGLjDG81qZToBHQDwoAnjjZhu1VCaBISuGbgZrxyyRyqdnn -xPW+KczMv04XyvF7v6Pz+bUEppalLXGiXnH5UtWvZQKBgQDTPCdMpNE/hwlq4nAw -Kf42zIBWfbnMLVWYoeDiAOhtl9XAUAXn76xe6Rvo0qeAo67yejdbJfRq3HvGyw+q -4PS8r9gXYmLYIPQxSoLL5+rFoBCN3qFippfjLB1j32mp7+15KjRj8FF2r6xIN8fu -XatSRsaqmvCWYLDRv/rbHnxwkwKBgQDLkyfFLF7BtwtPWKdqrwOM7ip1UKh+oDBS -vkCQ08aEFRBU7T3jChsx5GbaW6zmsSBwBwcrHclpSkz7n3aq19DDWObJR2p80Fma -rsXeIcvtEpkvT3pVX268P5d+XGs1kxgFunqTysG9yChW+xzcs5MdKBzuMPPn7rL8 -MKAzdar6PwKBgEypkzW8x3h/4Moa3k6MnwdyVs2NGaZheaRIc95yJ+jGZzxBjrMr -h+p2PbvU4BfO0AqOkpKRBtDVrlJqlggVVp04UHvEKE16QEW3Xhr0037f5cInX3j3 -Lz6yXwRFLAsR2aTUzWjL6jTh8uvO2s/GzQuyRh3a16Ar/WBShY+K0+zjAoGATnLT -xZjWnyHRmu8X/PWakamJ9RFzDPDgDlLAgM8LVgTj+UY/LgnL9wsEU6s2UuP5ExKy -QXxGDGwUhHar/SQTj+Pnc7Mwpw6HKSOmnnY5po8fNusSwml3O9XppEkrC0c236Y/ -7EobJO5IFVTJh4cv7vFxTJzSsRL8KFD4uzvh+nMCgYEAqY8NBYtIgNJA2B6C6hHF -+bG7v46434ZHFfGTmMQwzE4taVg7YRnzYESAlvK4bAP5ZXR90n7GRGFhrXzoMZ38 -r0bw/q9rV+ReGda7/Bjf7ciCKiq0RODcHtf4IaskjPXCoQRGJtgCPLhWPfld6g9v -/HTvO96xv9e3eG/PKSPog94= +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDZuQeMqkUCGB3s +h0lXK+IGPJWfEdFn2wYN8XKN//AV2DXthko8PFMKzZ16DuxFqrRfRGTu7OX1cQJg +Alt+ZonDRcTplk9NLT3yhdwhen6MeC6XfWUOW+5EDFYgVQxhvkUedHcoAPHsmee5 +eHYBHGXz52mBotGrDH+ACSgEq8F38gSo6mFxHm9txew3z0Katww6yZaW2A+sxlFA +E0Z72PU/odnc0/iRY98LOFaqzesRlRfRNIquj4nH1XzI2wGcd8XsqMtBrd8WUC7m +JBOjgOfZIP/x8zUP4vPjHTLtAOgRNxvQ4TavZXZPuH+eQgTLKXoPvgjBdSws0wXa +ShpPKH3JAgMBAAECggEAAuMgWEDGyam9Cy6vwzXsA/tS3Ek9yWk0ZJlxKd8SauZo +hVXspJgD3ZFkoffSMXSmJTBF6rbeBXn3ly3Y0u2JrKWqID3edIPMfeuWrnlOdc7p +2+ztWuTPB/x7j5IJKBUARiXdvFGnjk9r16q4XTiS79/U/Jjb5m8z6U8AcUVqJfdw +9+I4Yyr1RCQoRE+M07FSRYG2V8ejHlh1LtL3I+UEEcfYlTNWEMZcd//4bohM2Aku ++Fb5/KAnJ+1OjQjP1wzj8lSshbHF3vXtUgcsl64nMOUxYijnTvkt3INoHTHb0FY6 +SKDMUKr8SBf5ltINOz5IxgFAMJGKocrKfqVExdU5IQKBgQD0tjpUFldd3JqcbIgY +VUYynoPcUKAoGIgt8qdNR8+GziToI7ZDp18TOxxG62oobm0dvJ5BR22H8MIhn9iB +burp7yJvriZrL2wyHrr8EI7yepdu//O52rDfbJO4Irjh/URDuQyPK83IilZ3vO4U +RzI5wTS2epNV9IX11QZVN25jOQKBgQDjxBdW59hkxscWzjQy6fW1Z78oEzc5AaA+ +Xkp6NtBSP+2bFVBLD0DjkPGXRTmTdRLQz2eskSP8dyHxiErUEiMWZV85X0FgZFX5 +Z5r8LGOsWjoM2kAMj9nh44Ule/XJ+oF5tBjQOQ2XtF62UmYQfKVuaz/LivscNADh +8Kp5SVAfEQKBgGuhjswXO3wMIHC2h7F0KDjxYXvQdnDMQXE8Lfuenxdiqfb0ZiEh +h3602/4RYxK/ZvzSTiTWHsXQzgHuBVMAjxAvXs0SItG3/PWacJGXUtgxtVNb/j37 +gxnx7pLpqrmzJIhI5s497PfMaLWngmum2N9wLBgql40RzK3QcUWf6Mx5AoGBANUo +sYcd18EI304SkXuMxe2OOLIyuZ3aTbPQ3vbd0b0II0Deg5Sbo+jVv6QIn0fHa2KM +mMRB2WHvxI6dNRqgFsJhAOtaoH6rqGKPedbDXEzy7B0XLJYVEp57JiLcjj0G+qGB +0S8eFgCCR5luKCMJ5HEgYkYFvdi5OpI5f/GekSNhAoGBAKuosoMRPjMHKBBnxj13 +K1Fm6ZgRhokPNlWwsSJyoKxQnYQoneQjDQ+9DrDEYYg9sqAQVutm8dIPHncnhkUJ +yoq5PDuuNUZXTJVZ94Z7zTDDlZguli6o1uPptmOEC2iaA7iQKPw8R+ivCUKecojA +C0YBHVyE1tbHD7y9jQ9Cav36 -----END PRIVATE KEY----- diff --git a/testdata/init/server_intermediate.cnf b/testdata/init/server_intermediate.cnf new file mode 100644 index 000000000..aa906794c --- /dev/null +++ b/testdata/init/server_intermediate.cnf @@ -0,0 +1,30 @@ +[ req ] +default_bits = 2048 +distinguished_name = subject +req_extensions = req_ext +x509_extensions = x509_ext +string_mask = utf8only +prompt = no + +[ subject ] +C = US +ST = Nevada +L = Las Vegas +O = github.com/lib/pq +CN = localhost + +[ x509_ext ] +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer + +basicConstraints = CA:FALSE +keyUsage = digitalSignature, keyEncipherment +subjectAltName = DNS:localhost, IP:127.0.0.1 +nsComment = "OpenSSL Generated Certificate" + +[ req_ext ] +subjectKeyIdentifier = hash +basicConstraints = CA:FALSE +keyUsage = digitalSignature, keyEncipherment +subjectAltName = DNS:localhost, IP:127.0.0.1 +nsComment = "OpenSSL Generated Certificate" diff --git a/testdata/init/server_intermediate.crt b/testdata/init/server_intermediate.crt new file mode 100644 index 000000000..4de1e2a92 --- /dev/null +++ b/testdata/init/server_intermediate.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEATCCAumgAwIBAgIUMmQ+3iTA688OIt+AIz7OF87gohgwDQYJKoZIhvcNAQEL +BQAwazELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk5ldmFkYTESMBAGA1UEBwwJTGFz +IFZlZ2FzMRowGAYDVQQKDBFnaXRodWIuY29tL2xpYi9wcTEbMBkGA1UEAwwScHEg +SW50ZXJtZWRpYXRlIENBMB4XDTI2MDMwNTE3NDkwNloXDTM2MDMwNTE3NDkwNlow +YjELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk5ldmFkYTESMBAGA1UEBwwJTGFzIFZl +Z2FzMRowGAYDVQQKDBFnaXRodWIuY29tL2xpYi9wcTESMBAGA1UEAwwJbG9jYWxo +b3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs89bDblvNpdEko6/ +au3Vaq8QrRlTLsUWwvB4GeuGRZ/SLcm4Y7xcyPhbs4lbWbKcsq427ZyZ+qkGUHHL +s6vbLGjX53sW+PUM6EYRkHOl0FA/0lqN0FVOSWKiC4ZvdkTfSuY6/iTIWduy2ox4 +FAVDmwPk6Sg1e318x8F7vtSDYaEyuFdwsqHQGPEZf9AtldBsZ2re2t/yaAeAnxcc +WOYK8tA7moSS4Enr51zHtF8TsO0h2wW3gbKlU0w01qd/CVr2RmPEjWMlP0oi5kTi +Uq/R9LOc8kAGMZzBKdA0R/Qj2idhVJQzIcYKYSecvMsidJKCdndZslj5dD2LVqim +NW5HMQIDAQABo4GlMIGiMB0GA1UdDgQWBBQSTtu8/0nPNMLzdEOpWzZeoqn/azAJ +BgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A +AAEwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRl +MB8GA1UdIwQYMBaAFIxNhXgxVZ/2EP9wjcglaW6KZV9dMA0GCSqGSIb3DQEBCwUA +A4IBAQC02ZoWsETl58SxLILoQY/8SlkPu6eoosyu+6ApD0fZ8T7pbd3dqBRrfn+T +CCt6kXVb3oTHk8T4BDkk0Acle+Sle21zsDouyLJbF52AkaA0JQrvPx5A41VP/qpc +mV7RDHeIDf9F61+X5ArkBA0MZO1w37ojEWsFqvcpB60bN5FyMbBYG6E+ZlPg0ONk +9w2S/WvjkGBi8p+tU6zbAnvvAhcOAjZhwbZ7LLwYAvrenNf3KIbIMYoox2BQFUsw +Q5BvcfnAKntM3birKEYBCmH+bOlGgbmg/1mNlekh2tyfmMPw8tPt6OwL/HuYcY8K +3Omvln8yaHRdSL3lKAUIeXb5fn8O +-----END CERTIFICATE----- diff --git a/testdata/init/server_intermediate.key b/testdata/init/server_intermediate.key new file mode 100644 index 000000000..bc07e4bdd --- /dev/null +++ b/testdata/init/server_intermediate.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzz1sNuW82l0SS +jr9q7dVqrxCtGVMuxRbC8HgZ64ZFn9ItybhjvFzI+FuziVtZspyyrjbtnJn6qQZQ +ccuzq9ssaNfnexb49QzoRhGQc6XQUD/SWo3QVU5JYqILhm92RN9K5jr+JMhZ27La +jHgUBUObA+TpKDV7fXzHwXu+1INhoTK4V3CyodAY8Rl/0C2V0Gxnat7a3/JoB4Cf +FxxY5gry0DuahJLgSevnXMe0XxOw7SHbBbeBsqVTTDTWp38JWvZGY8SNYyU/SiLm +ROJSr9H0s5zyQAYxnMEp0DRH9CPaJ2FUlDMhxgphJ5y8yyJ0koJ2d1myWPl0PYtW +qKY1bkcxAgMBAAECggEABwdNMhtkQUBG9iqdyV58F+Q5dOa3RW3/ToXrT+oQsNKH +OvJ2YnGnt9Rbc0hkeXxdmy9rpryXGUD3pffYxFBsrA76F4qMcpVTmJW6lIisu9iT +MhQHlJPtEDnKh0RNQZR+HPWkazBBY5/OnTPd8rxk8N+FWGbRtl7InJ9PyL0SWKPN +U2GPQqFKYZ2Vx3D6lQso19HS59FUUMC+0Tx2jDqS4DYzFRBTWsWdWp+f8zHRX2yB +tW/o8zc3V8G1EPi2viE4T+GX5OTtjnTaQuEzBeg84nn3pHrCToAleQ5DFwFt1WEX +LP9I/vyBm05OvAghomjehxrHcLU2koCp30RRAjTHxQKBgQDukUDaxWhURb3EFXqy +SAPmwUw14i50tUrFfaSNk3g19VOCst03LFmc04dYOHymR1GS9SBpbLVqcU5Vp3MG +v7fFzIQXbWgdc/ZjEMam7VuTK0KuUUSTBKzpq9X2x/R3jUXRznBEDD3maGJ4vYXZ +CLY1t6KpJ+CxIZpqhj2FZqeijQKBgQDA8vXYCZmd6T0jMnJCdyvkqTnYDNTA60R7 +LRyJwWUHDWs0cbGPNTp4ebDz/IUPOk1Sy3Oxpr93ZOWuWZKvHaVxkkqgKPec2pnO +AESOYOGLY1nJPBYHMsme3FCERlx5x24PIMN5vLefrlRdGWQmKpRuIA8FLzEgzaEq +w6iInAggNQKBgHTCXHZ2BVCxbWXpiUp2Goq2ciExGMF/9R9hFcdAtKXb8spV1hTp +vNYXZPdVdhQ/dXoyRHG0hbmZyNf9Azv7WusQ5Fk+76Tym1Ty4fbS1m3Zz2HXXnOB +50raEfcc99YHK3O60JFNWIJK9l7XiwmkzODPhmm5nauzoYqfNr7ydfsFAoGBALZR +XWfjulnyGCj38+tF/B22oce6aBZauHzDpaGtMi81yMTnYWX2X+eS5VfGllxLNOE1 +CX8mFulUV4slbGs30iq8lvM7gq2eCZMTwbPfa39wQ1jZA9+NZ+JKP3KdoagYf7Cf +vtV7Mu8ZpPHLmkxOE67zb/3wF7XtV4q3Erry5OK5AoGBAJAfFFDkdvdQSsFdq+E/ +6ojeUq1KvAqD6pSnJzhl/5PuvjqG/OQHLGATeAsVVztWKJtF9NMouDskOB6fJVgi +T6UPjuHqudjByebs+rU3RtItBf/ejIzUGXPfQu5z1IoKw5D5qKByhOgDWQifz6WN +z836BFVSQH01OnGGLL3vOTAx +-----END PRIVATE KEY----- From 1e0ba6dfbc675b9072988edadd9e6552e0a654a9 Mon Sep 17 00:00:00 2001 From: Ben Chobot Date: Thu, 5 Mar 2026 10:25:25 -0800 Subject: [PATCH 03/11] Don't use mockPostgresSSLServer(); use a real server. There's no point in trying to be self-contained when so many other tests already require an external postgres server. And hey! Doing this reveals that our previous fix didn't actually fix things; the mock server was not sufficient in showing the real problem. So in addition to removing a lot of code, we also now have a bug fix that works against a real server. --- ssl.go | 10 +- ssl_intermediate_test.go | 365 +++++++-------------------------------- 2 files changed, 67 insertions(+), 308 deletions(-) diff --git a/ssl.go b/ssl.go index 2921f232b..8eec171d2 100644 --- a/ssl.go +++ b/ssl.go @@ -222,7 +222,15 @@ func sslClientCertificates(tlsConf *tls.Config, cfg Config) error { return err } - tlsConf.Certificates = []tls.Certificate{cert} + // Use GetClientCertificate instead of setting Certificates directly. + // 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 } diff --git a/ssl_intermediate_test.go b/ssl_intermediate_test.go index 0751ae468..d095bcd95 100644 --- a/ssl_intermediate_test.go +++ b/ssl_intermediate_test.go @@ -1,325 +1,76 @@ package pq import ( - "bytes" - "context" - "crypto/tls" - "crypto/x509" - "database/sql" - "encoding/pem" "fmt" - "io" - "net" "os" "testing" - "time" -) - -type mockSSLServerOpts struct { - serverCert tls.Certificate - clientCAs *x509.CertPool // nil means don't request client certs -} - -// mockPostgresSSLServer creates a mock PostgreSQL server with TLS. -// If opts.clientCAs is set, the server requests and manually verifies client -// certificates against that pool (simulating PostgreSQL's ssl_ca_file). -func mockPostgresSSLServer(t *testing.T, opts mockSSLServerOpts) (port string, errCh chan error) { - t.Helper() - - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { l.Close() }) - errCh = make(chan error, 1) - - go func() { - conn, err := l.Accept() - if err != nil { - errCh <- err - return - } - handleMockSSLConn(t, conn, opts, errCh) - }() - - _, port, _ = net.SplitHostPort(l.Addr().String()) - return port, errCh -} - -func handleMockSSLConn(t *testing.T, conn net.Conn, opts mockSSLServerOpts, errCh chan error) { - defer conn.Close() - conn.SetDeadline(time.Now().Add(5 * time.Second)) - - // Read SSL request message - startupMessage := make([]byte, 8) - if _, err := io.ReadFull(conn, startupMessage); err != nil { - errCh <- fmt.Errorf("reading startup: %w", err) - return - } - if !bytes.Equal(startupMessage, []byte{0, 0, 0, 0x8, 0x4, 0xd2, 0x16, 0x2f}) { - errCh <- fmt.Errorf("unexpected startup message: %#v", startupMessage) - return - } - // Respond with SSLOk - if _, err := conn.Write([]byte("S")); err != nil { - errCh <- fmt.Errorf("writing SSLOk: %w", err) - return - } - - // Configure TLS - tlsCfg := &tls.Config{ - Certificates: []tls.Certificate{opts.serverCert}, - } - if opts.clientCAs != nil { - // RequireAnyClientCert: request a client cert but don't let Go verify - // it automatically. We do manual verification after the handshake to - // simulate what PostgreSQL does. - tlsCfg.ClientAuth = tls.RequireAnyClientCert - } - - tlsConn := tls.Server(conn, tlsCfg) - if err := tlsConn.Handshake(); err != nil { - errCh <- fmt.Errorf("TLS handshake: %w", err) - return - } - defer tlsConn.Close() - - // Manually verify client cert chain if requested. - if opts.clientCAs != nil { - state := tlsConn.ConnectionState() - if len(state.PeerCertificates) == 0 { - errCh <- fmt.Errorf("client did not present a certificate") - return - } - intermediates := x509.NewCertPool() - for _, cert := range state.PeerCertificates[1:] { - intermediates.AddCert(cert) - } - _, err := state.PeerCertificates[0].Verify(x509.VerifyOptions{ - Roots: opts.clientCAs, - Intermediates: intermediates, - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - }) - if err != nil { - errCh <- fmt.Errorf("client cert verification failed: %w", err) - return - } - } - - // Read PostgreSQL startup message - buf := make([]byte, 4) - if _, err := io.ReadFull(tlsConn, buf); err != nil { - errCh <- err - return - } - length := int(buf[0])<<24 | int(buf[1])<<16 | int(buf[2])<<8 | int(buf[3]) - if length > 4 { - rest := make([]byte, length-4) - if _, err := io.ReadFull(tlsConn, rest); err != nil { - errCh <- err - return - } - } - - // AuthenticationOk + ReadyForQuery - tlsConn.Write([]byte{'R', 0, 0, 0, 8, 0, 0, 0, 0}) - tlsConn.Write([]byte{'Z', 0, 0, 0, 5, 'I'}) - - // Read client message (Ping sends a simple query) - msgType := make([]byte, 1) - if _, err := io.ReadFull(tlsConn, msgType); err != nil { - errCh <- err - return - } - if _, err := io.ReadFull(tlsConn, buf); err != nil { - errCh <- err - return - } - msgLen := int(buf[0])<<24 | int(buf[1])<<16 | int(buf[2])<<8 | int(buf[3]) - if msgLen > 4 { - body := make([]byte, msgLen-4) - if _, err := io.ReadFull(tlsConn, body); err != nil { - errCh <- err - return - } - } - - // CommandComplete + ReadyForQuery - tlsConn.Write([]byte{'C', 0, 0, 0, 11}) - tlsConn.Write([]byte("SELECT\x00")) - tlsConn.Write([]byte{'Z', 0, 0, 0, 5, 'I'}) - - close(errCh) // success - time.Sleep(100 * time.Millisecond) -} - -func pingMockServer(t *testing.T, dsn string, port string, errCh chan error) error { - t.Helper() - - connector, err := NewConnector(dsn) - if err != nil { - return err - } - db := sql.OpenDB(connector) - defer db.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - clientErr := db.PingContext(ctx) - - // Check server-side error first — it's more informative. - select { - case serverErr, ok := <-errCh: - if ok && serverErr != nil { - return fmt.Errorf("server: %s (client: %v)", serverErr, clientErr) - } - case <-time.After(2 * time.Second): - } + "github.com/lib/pq/internal/pqtest" +) - return clientErr -} +func TestSSLClientCertificateIntermediate(t *testing.T) { + pqtest.SkipPgpool(t) + pqtest.SkipPgbouncer(t) -// loadServerCert loads a TLS server certificate, optionally including the -// intermediate CA cert in the chain. -func loadServerCert(t *testing.T, certFile, keyFile, intermediateFile string) tls.Certificate { - t.Helper() + startSSLTest(t, "pqgosslcert") - cert, err := tls.LoadX509KeyPair(certFile, keyFile) + err := os.Chmod("testdata/init/client_intermediate.key", 0600) if err != nil { t.Fatal(err) } - if intermediateFile != "" { - interPEM, err := os.ReadFile(intermediateFile) - if err != nil { - t.Fatal(err) - } - block, _ := pem.Decode(interPEM) - if block != nil && block.Type == "CERTIFICATE" { - cert.Certificate = append(cert.Certificate, block.Bytes) - } - } - return cert -} - -// TestSSLIntermediateCA tests various intermediate CA scenarios for both -// server certificate verification (verify-ca, verify-full) and client -// certificate authentication. -func TestSSLIntermediateCA(t *testing.T) { - const ( - rootCert = "testdata/init/root.crt" - bundleCert = "testdata/init/root+intermediate.crt" - interCert = "testdata/init/intermediate.crt" - serverCert = "testdata/init/server_intermediate.crt" - serverKey = "testdata/init/server_intermediate.key" - clientCert = "testdata/init/client_intermediate.crt" - clientKey = "testdata/init/client_intermediate.key" - ) - - // Server cert with full chain [leaf, intermediate] - serverFullChain := loadServerCert(t, serverCert, serverKey, interCert) - // Server cert with only the leaf (no intermediate in chain) - serverLeafOnly := loadServerCert(t, serverCert, serverKey, "") - - t.Run("server cert verification", func(t *testing.T) { - tests := []struct { - name string - sslmode string - rootcert string - serverCert tls.Certificate - wantErr bool - }{ - // Server sends full chain [leaf, intermediate], sslrootcert has root only. - { - name: "verify-ca full chain root only", - sslmode: "verify-ca", - rootcert: rootCert, - serverCert: serverFullChain, - }, - { - name: "verify-full full chain root only", - sslmode: "verify-full", - rootcert: rootCert, - serverCert: serverFullChain, - }, - - // Server sends only leaf, sslrootcert has root+intermediate bundle. - { - name: "verify-ca leaf only bundle rootcert", - sslmode: "verify-ca", - rootcert: bundleCert, - serverCert: serverLeafOnly, - }, - { - name: "verify-full leaf only bundle rootcert", - sslmode: "verify-full", - rootcert: bundleCert, - serverCert: serverLeafOnly, - }, - - // Server sends only leaf, sslrootcert has root only — can't build chain. - { - name: "verify-ca leaf only root only fails", - sslmode: "verify-ca", - rootcert: rootCert, - serverCert: serverLeafOnly, - wantErr: true, - }, - { - name: "verify-full leaf only root only fails", - sslmode: "verify-full", - rootcert: rootCert, - serverCert: serverLeafOnly, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - port, errCh := mockPostgresSSLServer(t, mockSSLServerOpts{ - serverCert: tt.serverCert, - }) - dsn := fmt.Sprintf("host=127.0.0.1 port=%s sslmode=%s sslrootcert=%s user=test dbname=test connect_timeout=5", - port, tt.sslmode, tt.rootcert) - - err := pingMockServer(t, dsn, port, errCh) - if tt.wantErr && err == nil { - t.Fatal("expected error but got nil") + 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 { + 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 !tt.wantErr && err != nil { - t.Fatalf("expected no error but got: %s", err) + if err := rows.Close(); err != nil { + t.Fatal(err) } - }) - } - }) - - t.Run("client cert with intermediate CA", func(t *testing.T) { - // Server's CA trust store has only the root CA. It needs the client to - // send the intermediate cert in its TLS certificate chain. - rootPEM, err := os.ReadFile(rootCert) - if err != nil { - t.Fatal(err) - } - serverCAs := x509.NewCertPool() - serverCAs.AppendCertsFromPEM(rootPEM) - - if err := os.Chmod(clientKey, 0600); err != nil { - t.Fatal(err) - } - - port, errCh := mockPostgresSSLServer(t, mockSSLServerOpts{ - serverCert: serverFullChain, - clientCAs: serverCAs, + } }) - - dsn := fmt.Sprintf( - "host=127.0.0.1 port=%s sslmode=verify-ca sslrootcert=%s sslcert=%s sslkey=%s user=test dbname=test connect_timeout=5", - port, bundleCert, clientCert, clientKey) - - err = pingMockServer(t, dsn, port, errCh) - if err != nil { - t.Fatalf("client cert with intermediate CA failed: %s", err) - } - }) + } } From 2a46e43f640b2ec50500b314dd72a74f8db9a36c Mon Sep 17 00:00:00 2001 From: Ben Chobot Date: Thu, 5 Mar 2026 10:37:04 -0800 Subject: [PATCH 04/11] Fix pqtest/pqtest.go so all tests run cleanly. Go's x509.Certificate.Verify() returns a plain fmt.Errorf (not a typed error) for the "not standards compliant" check, so the type switch in InvalidCertificate couldn't match it. Add a string-based fallback for that case. --- internal/pqtest/pqtest.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/pqtest/pqtest.go b/internal/pqtest/pqtest.go index 96c9ee840..a809ef0de 100644 --- a/internal/pqtest/pqtest.go +++ b/internal/pqtest/pqtest.go @@ -44,11 +44,15 @@ func ForceBinaryParameters() bool { // InvalidCertificate reports if this error is an "invalid certificate" error. func InvalidCertificate(err error) bool { + if err == nil { + return false + } switch err.(type) { - case x509.UnknownAuthorityError, x509.HostnameError, *tls.CertificateVerificationError: + case x509.UnknownAuthorityError, x509.HostnameError, + x509.CertificateInvalidError, *tls.CertificateVerificationError: return true } - return false + return strings.Contains(err.Error(), "certificate is not standards compliant") } // Ptr gets a pointer to any value. From 3a09bf3a65602805d0c9b245145539f49147e3df Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Mon, 9 Mar 2026 13:35:48 +0000 Subject: [PATCH 05/11] Capture loop variable This wasn't changed until Go 1.22, and pq works with Go 1.21 --- ssl_intermediate_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ssl_intermediate_test.go b/ssl_intermediate_test.go index d095bcd95..5a630f4e2 100644 --- a/ssl_intermediate_test.go +++ b/ssl_intermediate_test.go @@ -56,6 +56,7 @@ func TestSSLClientCertificateIntermediate(t *testing.T) { } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() db, err := openSSLConn(t, tt.connect) From f8a9afe2a1f9d040631f4a6de623d54b3cf8cead Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Mon, 9 Mar 2026 13:37:24 +0000 Subject: [PATCH 06/11] Move test to ssl_test.go --- ssl_intermediate_test.go | 77 ---------------------------------------- ssl_test.go | 67 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 77 deletions(-) delete mode 100644 ssl_intermediate_test.go diff --git a/ssl_intermediate_test.go b/ssl_intermediate_test.go deleted file mode 100644 index 5a630f4e2..000000000 --- a/ssl_intermediate_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package pq - -import ( - "fmt" - "os" - "testing" - - "github.com/lib/pq/internal/pqtest" -) - -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) - } - } - }) - } -} diff --git a/ssl_test.go b/ssl_test.go index af038cebc..132068934 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -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 From fa3011770e561f8bb90171a48d9a42c5ab9a721b Mon Sep 17 00:00:00 2001 From: Ben Chobot Date: Mon, 9 Mar 2026 15:54:10 -0700 Subject: [PATCH 07/11] Fix both file and inline intermediate certs. 1. Inline intermediate certs were completely ignored. In giving them some love, discover another pre-existing bug, where os.Stat was trying to read inlined-PEM data when it expected a file path. Of course there is no file path for an inline cert. 2. File intermediate certs where sending a cert but without the intermediate chain. This patch fixes all those problems. All tests pass. --- ssl.go | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/ssl.go b/ssl.go index 8eec171d2..369c6e5c9 100644 --- a/ssl.go +++ b/ssl.go @@ -87,7 +87,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 = "" @@ -167,7 +169,15 @@ func sslClientCertificates(tlsConf *tls.Config, cfg Config) error { if err != nil { return err } - tlsConf.Certificates = []tls.Certificate{cert} + // Use GetClientCertificate instead of setting Certificates directly. + // 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 } @@ -239,7 +249,7 @@ func sslClientCertificates(tlsConf *tls.Config, cfg Config) error { // 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) { - if len(tlsConf.Certificates) == 0 || cfg.SSLRootCert == "" { + if tlsConf.GetClientCertificate == nil || cfg.SSLRootCert == "" { return } @@ -254,6 +264,7 @@ func sslAppendIntermediates(tlsConf *tls.Config, cfg Config) { } } + var intermediates [][]byte for { var block *pem.Block block, pemData = pem.Decode(pemData) @@ -269,8 +280,24 @@ func sslAppendIntermediates(tlsConf *tls.Config, cfg Config) { } // Skip self-signed root CAs; only append intermediates. if cert.IsCA && !bytes.Equal(cert.RawIssuer, cert.RawSubject) { - tlsConf.Certificates[0].Certificate = append(tlsConf.Certificates[0].Certificate, block.Bytes) + intermediates = append(intermediates, block.Bytes) + } + } + + if len(intermediates) == 0 { + return + } + + // 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 } } From cbf10d5ad4f5b813eb4b57321f88a0dbe534746e Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Tue, 10 Mar 2026 14:51:19 +0000 Subject: [PATCH 08/11] pqtest.InvalidCertificate() doesn't need to be updated? Tests seem to work fine with the old version? --- internal/pqtest/pqtest.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/internal/pqtest/pqtest.go b/internal/pqtest/pqtest.go index a809ef0de..96c9ee840 100644 --- a/internal/pqtest/pqtest.go +++ b/internal/pqtest/pqtest.go @@ -44,15 +44,11 @@ func ForceBinaryParameters() bool { // InvalidCertificate reports if this error is an "invalid certificate" error. func InvalidCertificate(err error) bool { - if err == nil { - return false - } switch err.(type) { - case x509.UnknownAuthorityError, x509.HostnameError, - x509.CertificateInvalidError, *tls.CertificateVerificationError: + case x509.UnknownAuthorityError, x509.HostnameError, *tls.CertificateVerificationError: return true } - return strings.Contains(err.Error(), "certificate is not standards compliant") + return false } // Ptr gets a pointer to any value. From dfa76974c583484ddd0e3b35d9c93779e0dd8117 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Tue, 10 Mar 2026 15:11:55 +0000 Subject: [PATCH 09/11] Don't read cfg.SSLRootCert twice sslCertificateAuthority() would already read this, so return it from there. --- ssl.go | 75 +++++++++++++++++++++++++--------------------------------- 1 file changed, 32 insertions(+), 43 deletions(-) diff --git a/ssl.go b/ssl.go index 369c6e5c9..9aecf68ae 100644 --- a/ssl.go +++ b/ssl.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strings" "sync" "syscall" @@ -122,11 +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) + sslAppendIntermediates(tlsConf, cfg, rootPem) // Accept renegotiation requests initiated by the backend. // @@ -169,8 +170,8 @@ func sslClientCertificates(tlsConf *tls.Config, cfg Config) error { if err != nil { return err } - // Use GetClientCertificate instead of setting Certificates directly. - // When Certificates is set, Go's TLS client only sends the cert if the + // 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. @@ -232,12 +233,8 @@ func sslClientCertificates(tlsConf *tls.Config, cfg Config) error { return err } - // Use GetClientCertificate instead of setting Certificates directly. - // 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. + // Using GetClientCertificate instead of Certificates field as per comment + // above. tlsConf.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { return &cert, nil } @@ -248,23 +245,15 @@ func sslClientCertificates(tlsConf *tls.Config, cfg Config) error { // 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) { - if tlsConf.GetClientCertificate == nil || cfg.SSLRootCert == "" { +func sslAppendIntermediates(tlsConf *tls.Config, cfg Config, rootPem []byte) { + if cfg.SSLRootCert == "" || tlsConf.GetClientCertificate == nil || len(rootPem) == 0 { return } - var pemData []byte - if cfg.SSLInline { - pemData = []byte(cfg.SSLRootCert) - } else { - var err error - pemData, err = os.ReadFile(cfg.SSLRootCert) - if err != nil { - return - } - } - - var intermediates [][]byte + var ( + pemData = slices.Clone(rootPem) + intermediates [][]byte + ) for { var block *pem.Block block, pemData = pem.Decode(pemData) @@ -283,13 +272,12 @@ func sslAppendIntermediates(tlsConf *tls.Config, cfg Config) { intermediates = append(intermediates, block.Bytes) } } - if len(intermediates) == 0 { return } - // Wrap the existing GetClientCertificate to append intermediate certs - // to the certificate chain returned during the TLS handshake. + // 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) @@ -302,28 +290,29 @@ func sslAppendIntermediates(tlsConf *tls.Config, cfg Config) { } // 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() - if !tlsConf.RootCAs.AppendCertsFromPEM(cert) { - return errors.New("pq: couldn't parse pem in sslrootcert") + 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 } } - return nil + if !tlsConf.RootCAs.AppendCertsFromPEM(cert) { + return nil, errors.New("pq: couldn't parse pem in sslrootcert") + } + return cert, nil } From 9d568b93f4d2e8c158d48898920149205b0b1f28 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Tue, 10 Mar 2026 15:13:15 +0000 Subject: [PATCH 10/11] order --- ssl.go | 56 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/ssl.go b/ssl.go index 9aecf68ae..bf20f0ced 100644 --- a/ssl.go +++ b/ssl.go @@ -241,6 +241,34 @@ func sslClientCertificates(tlsConf *tls.Config, cfg Config) error { return nil } +// sslCertificateAuthority adds the RootCA specified in the "sslrootcert" setting. +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 cfg.SSLRootCert == "" { + return nil, nil + } + + 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 @@ -288,31 +316,3 @@ func sslAppendIntermediates(tlsConf *tls.Config, cfg Config, rootPem []byte) { return cert, nil } } - -// sslCertificateAuthority adds the RootCA specified in the "sslrootcert" setting. -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 cfg.SSLRootCert == "" { - return nil, nil - } - - 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 -} From 5cab9c5b54de444b846412e21c1e5c37fd441159 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Tue, 10 Mar 2026 15:21:07 +0000 Subject: [PATCH 11/11] Add changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86d65ca31..28ce73d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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