1package pq
2
3// This file contains SSL tests
4
5import (
6	_ "crypto/sha256"
7	"crypto/x509"
8	"database/sql"
9	"os"
10	"path/filepath"
11	"testing"
12)
13
14func maybeSkipSSLTests(t *testing.T) {
15	// Require some special variables for testing certificates
16	if os.Getenv("PQSSLCERTTEST_PATH") == "" {
17		t.Skip("PQSSLCERTTEST_PATH not set, skipping SSL tests")
18	}
19
20	value := os.Getenv("PQGOSSLTESTS")
21	if value == "" || value == "0" {
22		t.Skip("PQGOSSLTESTS not enabled, skipping SSL tests")
23	} else if value != "1" {
24		t.Fatalf("unexpected value %q for PQGOSSLTESTS", value)
25	}
26}
27
28func openSSLConn(t *testing.T, conninfo string) (*sql.DB, error) {
29	db, err := openTestConnConninfo(conninfo)
30	if err != nil {
31		// should never fail
32		t.Fatal(err)
33	}
34	// Do something with the connection to see whether it's working or not.
35	tx, err := db.Begin()
36	if err == nil {
37		return db, tx.Rollback()
38	}
39	_ = db.Close()
40	return nil, err
41}
42
43func checkSSLSetup(t *testing.T, conninfo string) {
44	_, err := openSSLConn(t, conninfo)
45	if pge, ok := err.(*Error); ok {
46		if pge.Code.Name() != "invalid_authorization_specification" {
47			t.Fatalf("unexpected error code '%s'", pge.Code.Name())
48		}
49	} else {
50		t.Fatalf("expected %T, got %v", (*Error)(nil), err)
51	}
52}
53
54// Connect over SSL and run a simple query to test the basics
55func TestSSLConnection(t *testing.T) {
56	maybeSkipSSLTests(t)
57	// Environment sanity check: should fail without SSL
58	checkSSLSetup(t, "sslmode=disable user=pqgossltest")
59
60	db, err := openSSLConn(t, "sslmode=require user=pqgossltest")
61	if err != nil {
62		t.Fatal(err)
63	}
64	rows, err := db.Query("SELECT 1")
65	if err != nil {
66		t.Fatal(err)
67	}
68	rows.Close()
69}
70
71// Test sslmode=verify-full
72func TestSSLVerifyFull(t *testing.T) {
73	maybeSkipSSLTests(t)
74	// Environment sanity check: should fail without SSL
75	checkSSLSetup(t, "sslmode=disable user=pqgossltest")
76
77	// Not OK according to the system CA
78	_, err := openSSLConn(t, "host=postgres sslmode=verify-full user=pqgossltest")
79	if err == nil {
80		t.Fatal("expected error")
81	}
82	_, ok := err.(x509.UnknownAuthorityError)
83	if !ok {
84		t.Fatalf("expected x509.UnknownAuthorityError, got %#+v", err)
85	}
86
87	rootCertPath := filepath.Join(os.Getenv("PQSSLCERTTEST_PATH"), "root.crt")
88	rootCert := "sslrootcert=" + rootCertPath + " "
89	// No match on Common Name
90	_, err = openSSLConn(t, rootCert+"host=127.0.0.1 sslmode=verify-full user=pqgossltest")
91	if err == nil {
92		t.Fatal("expected error")
93	}
94	_, ok = err.(x509.HostnameError)
95	if !ok {
96		t.Fatalf("expected x509.HostnameError, got %#+v", err)
97	}
98	// OK
99	_, err = openSSLConn(t, rootCert+"host=postgres sslmode=verify-full user=pqgossltest")
100	if err != nil {
101		t.Fatal(err)
102	}
103}
104
105// Test sslmode=require sslrootcert=rootCertPath
106func TestSSLRequireWithRootCert(t *testing.T) {
107	maybeSkipSSLTests(t)
108	// Environment sanity check: should fail without SSL
109	checkSSLSetup(t, "sslmode=disable user=pqgossltest")
110
111	bogusRootCertPath := filepath.Join(os.Getenv("PQSSLCERTTEST_PATH"), "bogus_root.crt")
112	bogusRootCert := "sslrootcert=" + bogusRootCertPath + " "
113
114	// Not OK according to the bogus CA
115	_, err := openSSLConn(t, bogusRootCert+"host=postgres sslmode=require user=pqgossltest")
116	if err == nil {
117		t.Fatal("expected error")
118	}
119	_, ok := err.(x509.UnknownAuthorityError)
120	if !ok {
121		t.Fatalf("expected x509.UnknownAuthorityError, got %s, %#+v", err, err)
122	}
123
124	nonExistentCertPath := filepath.Join(os.Getenv("PQSSLCERTTEST_PATH"), "non_existent.crt")
125	nonExistentCert := "sslrootcert=" + nonExistentCertPath + " "
126
127	// No match on Common Name, but that's OK because we're not validating anything.
128	_, err = openSSLConn(t, nonExistentCert+"host=127.0.0.1 sslmode=require user=pqgossltest")
129	if err != nil {
130		t.Fatal(err)
131	}
132
133	rootCertPath := filepath.Join(os.Getenv("PQSSLCERTTEST_PATH"), "root.crt")
134	rootCert := "sslrootcert=" + rootCertPath + " "
135
136	// No match on Common Name, but that's OK because we're not validating the CN.
137	_, err = openSSLConn(t, rootCert+"host=127.0.0.1 sslmode=require user=pqgossltest")
138	if err != nil {
139		t.Fatal(err)
140	}
141	// Everything OK
142	_, err = openSSLConn(t, rootCert+"host=postgres sslmode=require user=pqgossltest")
143	if err != nil {
144		t.Fatal(err)
145	}
146}
147
148// Test sslmode=verify-ca
149func TestSSLVerifyCA(t *testing.T) {
150	maybeSkipSSLTests(t)
151	// Environment sanity check: should fail without SSL
152	checkSSLSetup(t, "sslmode=disable user=pqgossltest")
153
154	// Not OK according to the system CA
155	{
156		_, err := openSSLConn(t, "host=postgres sslmode=verify-ca user=pqgossltest")
157		if _, ok := err.(x509.UnknownAuthorityError); !ok {
158			t.Fatalf("expected %T, got %#+v", x509.UnknownAuthorityError{}, err)
159		}
160	}
161
162	// Still not OK according to the system CA; empty sslrootcert is treated as unspecified.
163	{
164		_, err := openSSLConn(t, "host=postgres sslmode=verify-ca user=pqgossltest sslrootcert=''")
165		if _, ok := err.(x509.UnknownAuthorityError); !ok {
166			t.Fatalf("expected %T, got %#+v", x509.UnknownAuthorityError{}, err)
167		}
168	}
169
170	rootCertPath := filepath.Join(os.Getenv("PQSSLCERTTEST_PATH"), "root.crt")
171	rootCert := "sslrootcert=" + rootCertPath + " "
172	// No match on Common Name, but that's OK
173	if _, err := openSSLConn(t, rootCert+"host=127.0.0.1 sslmode=verify-ca user=pqgossltest"); err != nil {
174		t.Fatal(err)
175	}
176	// Everything OK
177	if _, err := openSSLConn(t, rootCert+"host=postgres sslmode=verify-ca user=pqgossltest"); err != nil {
178		t.Fatal(err)
179	}
180}
181
182// Authenticate over SSL using client certificates
183func TestSSLClientCertificates(t *testing.T) {
184	maybeSkipSSLTests(t)
185	// Environment sanity check: should fail without SSL
186	checkSSLSetup(t, "sslmode=disable user=pqgossltest")
187
188	const baseinfo = "sslmode=require user=pqgosslcert"
189
190	// Certificate not specified, should fail
191	{
192		_, err := openSSLConn(t, baseinfo)
193		if pge, ok := err.(*Error); ok {
194			if pge.Code.Name() != "invalid_authorization_specification" {
195				t.Fatalf("unexpected error code '%s'", pge.Code.Name())
196			}
197		} else {
198			t.Fatalf("expected %T, got %v", (*Error)(nil), err)
199		}
200	}
201
202	// Empty certificate specified, should fail
203	{
204		_, err := openSSLConn(t, baseinfo+" sslcert=''")
205		if pge, ok := err.(*Error); ok {
206			if pge.Code.Name() != "invalid_authorization_specification" {
207				t.Fatalf("unexpected error code '%s'", pge.Code.Name())
208			}
209		} else {
210			t.Fatalf("expected %T, got %v", (*Error)(nil), err)
211		}
212	}
213
214	// Non-existent certificate specified, should fail
215	{
216		_, err := openSSLConn(t, baseinfo+" sslcert=/tmp/filedoesnotexist")
217		if pge, ok := err.(*Error); ok {
218			if pge.Code.Name() != "invalid_authorization_specification" {
219				t.Fatalf("unexpected error code '%s'", pge.Code.Name())
220			}
221		} else {
222			t.Fatalf("expected %T, got %v", (*Error)(nil), err)
223		}
224	}
225
226	certpath, ok := os.LookupEnv("PQSSLCERTTEST_PATH")
227	if !ok {
228		t.Fatalf("PQSSLCERTTEST_PATH not present in environment")
229	}
230
231	sslcert := filepath.Join(certpath, "postgresql.crt")
232
233	// Cert present, key not specified, should fail
234	{
235		_, err := openSSLConn(t, baseinfo+" sslcert="+sslcert)
236		if _, ok := err.(*os.PathError); !ok {
237			t.Fatalf("expected %T, got %#+v", (*os.PathError)(nil), err)
238		}
239	}
240
241	// Cert present, empty key specified, should fail
242	{
243		_, err := openSSLConn(t, baseinfo+" sslcert="+sslcert+" sslkey=''")
244		if _, ok := err.(*os.PathError); !ok {
245			t.Fatalf("expected %T, got %#+v", (*os.PathError)(nil), err)
246		}
247	}
248
249	// Cert present, non-existent key, should fail
250	{
251		_, err := openSSLConn(t, baseinfo+" sslcert="+sslcert+" sslkey=/tmp/filedoesnotexist")
252		if _, ok := err.(*os.PathError); !ok {
253			t.Fatalf("expected %T, got %#+v", (*os.PathError)(nil), err)
254		}
255	}
256
257	// Key has wrong permissions (passing the cert as the key), should fail
258	if _, err := openSSLConn(t, baseinfo+" sslcert="+sslcert+" sslkey="+sslcert); err != ErrSSLKeyHasWorldPermissions {
259		t.Fatalf("expected %s, got %#+v", ErrSSLKeyHasWorldPermissions, err)
260	}
261
262	sslkey := filepath.Join(certpath, "postgresql.key")
263
264	// Should work
265	if db, err := openSSLConn(t, baseinfo+" sslcert="+sslcert+" sslkey="+sslkey); err != nil {
266		t.Fatal(err)
267	} else {
268		rows, err := db.Query("SELECT 1")
269		if err != nil {
270			t.Fatal(err)
271		}
272		if err := rows.Close(); err != nil {
273			t.Fatal(err)
274		}
275		if err := db.Close(); err != nil {
276			t.Fatal(err)
277		}
278	}
279}
280