Skip to content

Commit 8eda731

Browse files
Allow to use system CA pool for certificate verification
This adds a new option to libpq's sslrootcert, "system", which will load the system trusted CA roots for certificate verification. This is a more convenient way to achieve this than pointing to the system CA roots manually since the location can differ by installation and be locally adjusted by env vars in OpenSSL. When sslrootcert is set to system, sslmode is forced to be verify-full as weaker modes aren't providing much security for public CAs. Changing the location of the system roots by setting environment vars is not supported by LibreSSL so the tests will use a heuristic to determine if the system being tested is LibreSSL or OpenSSL. The workaround in .cirrus.yml is required to handle a strange interaction between homebrew and the openssl@3 formula; hopefully this can be removed in the near future. The original patch was written by Thomas Habets, which was later revived by Jacob Champion. Author: Jacob Champion <[email protected]> Author: Thomas Habets <[email protected]> Reviewed-by: Jelte Fennema <[email protected]> Reviewed-by: Andrew Dunstan <[email protected]> Reviewed-by: Magnus Hagander <[email protected]> Discussion: https://www.postgresql.org/message-id/flat/CA%2BkHd%2BcJwCUxVb-Gj_0ptr3_KZPwi3%2B67vK6HnLFBK9MzuYrLA%40mail.gmail.com
1 parent 12f3867 commit 8eda731

File tree

9 files changed

+247
-9
lines changed

9 files changed

+247
-9
lines changed

.cirrus.yml

+13-1
Original file line numberDiff line numberDiff line change
@@ -477,12 +477,24 @@ task:
477477
make \
478478
meson \
479479
openldap \
480-
openssl \
480+
openssl@3 \
481481
python \
482482
tcl-tk \
483483
zstd
484484
485485
brew cleanup -s # to reduce cache size
486+
487+
# brew cleanup removes the empty certs directory in OPENSSLDIR, causing
488+
# OpenSSL to report unexpected errors ("unregistered scheme") during
489+
# verification failures. Put it back for now as a workaround.
490+
#
491+
# https://github.com/orgs/Homebrew/discussions/4030
492+
#
493+
# Note that $(brew --prefix openssl) will give us the opt/ prefix but not
494+
# the etc/ prefix, so we hardcode the full path here. openssl@3 is pinned
495+
# above to try to minimize the chances of this changing beneath us, but it's
496+
# brittle...
497+
mkdir -p "/opt/homebrew/etc/openssl@3/certs"
486498
upload_caches: homebrew
487499

488500
ccache_cache:

doc/src/sgml/libpq.sgml

+24
Original file line numberDiff line numberDiff line change
@@ -1876,6 +1876,30 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
18761876
to be signed by one of these authorities. The default is
18771877
<filename>~/.postgresql/root.crt</filename>.
18781878
</para>
1879+
<para>
1880+
The special value <literal>system</literal> may be specified instead, in
1881+
which case the system's trusted CA roots will be loaded. The exact
1882+
locations of these root certificates differ by SSL implementation and
1883+
platform. For <productname>OpenSSL</productname> in particular, the
1884+
locations may be further modified by the <envar>SSL_CERT_DIR</envar>
1885+
and <envar>SSL_CERT_FILE</envar> environment variables.
1886+
</para>
1887+
<note>
1888+
<para>
1889+
When using <literal>sslrootcert=system</literal>, the default
1890+
<literal>sslmode</literal> is changed to <literal>verify-full</literal>,
1891+
and any weaker setting will result in an error. In most cases it is
1892+
trivial for anyone to obtain a certificate trusted by the system for a
1893+
hostname they control, rendering <literal>verify-ca</literal> and all
1894+
weaker modes useless.
1895+
</para>
1896+
<para>
1897+
The magic <literal>system</literal> value will take precedence over a
1898+
local certificate file with the same name. If for some reason you find
1899+
yourself in this situation, use an alternative path like
1900+
<literal>sslrootcert=./system</literal> instead.
1901+
</para>
1902+
</note>
18791903
</listitem>
18801904
</varlistentry>
18811905

doc/src/sgml/runtime.sgml

+5-1
Original file line numberDiff line numberDiff line change
@@ -2007,7 +2007,11 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
20072007
(<xref linkend="ssl-tcp"/>). The TCP client must connect using
20082008
<literal>sslmode=verify-ca</literal> or
20092009
<literal>verify-full</literal> and have the appropriate root certificate
2010-
file installed (<xref linkend="libq-ssl-certificates"/>).
2010+
file installed (<xref linkend="libq-ssl-certificates"/>). Alternatively the
2011+
system CA pool can be used using <literal>sslrootcert=system</literal>; in
2012+
this case, <literal>sslmode=verify-full</literal> is forced for safety, since
2013+
it is generally trivial to obtain certificates which are signed by a public
2014+
CA.
20112015
</para>
20122016

20132017
<para>

src/interfaces/libpq/fe-connect.c

+66
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,23 @@ connectOptions2(PGconn *conn)
14651465
goto oom_error;
14661466
}
14671467

1468+
#ifndef USE_SSL
1469+
1470+
/*
1471+
* sslrootcert=system is not supported. Since setting this changes the
1472+
* default sslmode, check this _before_ we validate sslmode, to avoid
1473+
* confusing the user with errors for an option they may not have set.
1474+
*/
1475+
if (conn->sslrootcert
1476+
&& strcmp(conn->sslrootcert, "system") == 0)
1477+
{
1478+
conn->status = CONNECTION_BAD;
1479+
libpq_append_conn_error(conn, "sslrootcert value \"%s\" invalid when SSL support is not compiled in",
1480+
conn->sslrootcert);
1481+
return false;
1482+
}
1483+
#endif
1484+
14681485
/*
14691486
* validate sslmode option
14701487
*/
@@ -1511,6 +1528,22 @@ connectOptions2(PGconn *conn)
15111528
goto oom_error;
15121529
}
15131530

1531+
#ifdef USE_SSL
1532+
1533+
/*
1534+
* If sslrootcert=system, make sure our chosen sslmode is compatible.
1535+
*/
1536+
if (conn->sslrootcert
1537+
&& strcmp(conn->sslrootcert, "system") == 0
1538+
&& strcmp(conn->sslmode, "verify-full") != 0)
1539+
{
1540+
conn->status = CONNECTION_BAD;
1541+
libpq_append_conn_error(conn, "weak sslmode \"%s\" may not be used with sslrootcert=system (use verify-full)",
1542+
conn->sslmode);
1543+
return false;
1544+
}
1545+
#endif
1546+
15141547
/*
15151548
* Validate TLS protocol versions for ssl_min_protocol_version and
15161549
* ssl_max_protocol_version.
@@ -6236,6 +6269,8 @@ static bool
62366269
conninfo_add_defaults(PQconninfoOption *options, PQExpBuffer errorMessage)
62376270
{
62386271
PQconninfoOption *option;
6272+
PQconninfoOption *sslmode_default = NULL,
6273+
*sslrootcert = NULL;
62396274
char *tmp;
62406275

62416276
/*
@@ -6252,6 +6287,9 @@ conninfo_add_defaults(PQconninfoOption *options, PQExpBuffer errorMessage)
62526287
*/
62536288
for (option = options; option->keyword != NULL; option++)
62546289
{
6290+
if (strcmp(option->keyword, "sslrootcert") == 0)
6291+
sslrootcert = option; /* save for later */
6292+
62556293
if (option->val != NULL)
62566294
continue; /* Value was in conninfo or service */
62576295

@@ -6294,6 +6332,13 @@ conninfo_add_defaults(PQconninfoOption *options, PQExpBuffer errorMessage)
62946332
}
62956333
continue;
62966334
}
6335+
6336+
/*
6337+
* sslmode is not specified. Let it be filled in with the compiled
6338+
* default for now, but if sslrootcert=system, we'll override the
6339+
* default later before returning.
6340+
*/
6341+
sslmode_default = option;
62976342
}
62986343

62996344
/*
@@ -6326,6 +6371,27 @@ conninfo_add_defaults(PQconninfoOption *options, PQExpBuffer errorMessage)
63266371
}
63276372
}
63286373

6374+
/*
6375+
* Special handling for sslrootcert=system with no sslmode explicitly
6376+
* defined. In this case we want to strengthen the default sslmode to
6377+
* verify-full.
6378+
*/
6379+
if (sslmode_default && sslrootcert)
6380+
{
6381+
if (sslrootcert->val && strcmp(sslrootcert->val, "system") == 0)
6382+
{
6383+
free(sslmode_default->val);
6384+
6385+
sslmode_default->val = strdup("verify-full");
6386+
if (!sslmode_default->val)
6387+
{
6388+
if (errorMessage)
6389+
libpq_append_error(errorMessage, "out of memory");
6390+
return false;
6391+
}
6392+
}
6393+
}
6394+
63296395
return true;
63306396
}
63316397

src/interfaces/libpq/fe-secure-openssl.c

+25-4
Original file line numberDiff line numberDiff line change
@@ -1060,8 +1060,29 @@ initialize_SSL(PGconn *conn)
10601060
else
10611061
fnbuf[0] = '\0';
10621062

1063-
if (fnbuf[0] != '\0' &&
1064-
stat(fnbuf, &buf) == 0)
1063+
if (strcmp(fnbuf, "system") == 0)
1064+
{
1065+
/*
1066+
* The "system" sentinel value indicates that we should load whatever
1067+
* root certificates are installed for use by OpenSSL; these locations
1068+
* differ by platform. Note that the default system locations may be
1069+
* further overridden by the SSL_CERT_DIR and SSL_CERT_FILE
1070+
* environment variables.
1071+
*/
1072+
if (SSL_CTX_set_default_verify_paths(SSL_context) != 1)
1073+
{
1074+
char *err = SSLerrmessage(ERR_get_error());
1075+
1076+
libpq_append_conn_error(conn, "could not load system root certificate paths: %s",
1077+
err);
1078+
SSLerrfree(err);
1079+
SSL_CTX_free(SSL_context);
1080+
return -1;
1081+
}
1082+
have_rootcert = true;
1083+
}
1084+
else if (fnbuf[0] != '\0' &&
1085+
stat(fnbuf, &buf) == 0)
10651086
{
10661087
X509_STORE *cvstore;
10671088

@@ -1122,10 +1143,10 @@ initialize_SSL(PGconn *conn)
11221143
*/
11231144
if (fnbuf[0] == '\0')
11241145
libpq_append_conn_error(conn, "could not get home directory to locate root certificate file\n"
1125-
"Either provide the file or change sslmode to disable server certificate verification.");
1146+
"Either provide the file, use the system's trusted roots with sslrootcert=system, or change sslmode to disable server certificate verification.");
11261147
else
11271148
libpq_append_conn_error(conn, "root certificate file \"%s\" does not exist\n"
1128-
"Either provide the file or change sslmode to disable server certificate verification.", fnbuf);
1149+
"Either provide the file, use the system's trusted roots with sslrootcert=system, or change sslmode to disable server certificate verification.", fnbuf);
11291150
SSL_CTX_free(SSL_context);
11301151
return -1;
11311152
}

src/interfaces/libpq/t/001_uri.pl

+28-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99

1010
# List of URIs tests. For each test the first element is the input string, the
11-
# second the expected stdout and the third the expected stderr.
11+
# second the expected stdout and the third the expected stderr. Optionally,
12+
# additional arguments may specify key/value pairs which will override
13+
# environment variables for the duration of the test.
1214
my @tests = (
1315
[
1416
q{postgresql://uri-user:secret@host:12345/db},
@@ -209,20 +211,44 @@
209211
q{postgres://%2Fvar%2Flib%2Fpostgresql/dbname},
210212
q{dbname='dbname' host='/var/lib/postgresql' (local)},
211213
q{},
214+
],
215+
# Usually the default sslmode is 'prefer' (for libraries with SSL) or
216+
# 'disable' (for those without). This default changes to 'verify-full' if
217+
# the system CA store is in use.
218+
[
219+
q{postgresql://host?sslmode=disable},
220+
q{host='host' sslmode='disable' (inet)},
221+
q{},
222+
PGSSLROOTCERT => "system",
223+
],
224+
[
225+
q{postgresql://host?sslmode=prefer},
226+
q{host='host' sslmode='prefer' (inet)},
227+
q{},
228+
PGSSLROOTCERT => "system",
229+
],
230+
[
231+
q{postgresql://host?sslmode=verify-full},
232+
q{host='host' (inet)},
233+
q{},
234+
PGSSLROOTCERT => "system",
212235
]);
213236

214237
# test to run for each of the above test definitions
215238
sub test_uri
216239
{
217240
local $Test::Builder::Level = $Test::Builder::Level + 1;
241+
local %ENV = %ENV;
218242

219243
my $uri;
220244
my %expect;
245+
my %envvars;
221246
my %result;
222247

223-
($uri, $expect{stdout}, $expect{stderr}) = @$_;
248+
($uri, $expect{stdout}, $expect{stderr}, %envvars) = @$_;
224249

225250
$expect{'exit'} = $expect{stderr} eq '';
251+
%ENV = (%ENV, %envvars);
226252

227253
my $cmd = [ 'libpq_uri_regress', $uri ];
228254
$result{exit} = IPC::Run::run $cmd, '>', \$result{stdout}, '2>',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDAzCCAesCCCAhAwMUEgcBMA0GCSqGSIb3DQEBCwUAMEIxQDA+BgNVBAMMN1Rl
3+
c3QgQ0EgZm9yIFBvc3RncmVTUUwgU1NMIHJlZ3Jlc3Npb24gdGVzdCBzZXJ2ZXIg
4+
Y2VydHMwHhcNMjEwMzAzMjIxMjA3WhcNNDgwNzE5MjIxMjA3WjBGMR4wHAYDVQQL
5+
DBVQb3N0Z3JlU1FMIHRlc3Qgc3VpdGUxJDAiBgNVBAMMG2NvbW1vbi1uYW1lLnBn
6+
LXNzbHRlc3QudGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANWz
7+
VPMk7i5f+W0eEadRE+TTAtsIK08CkLMUnjs7zJkxnnm6RGBXPx6vK3AkAIi+wG4Y
8+
mXjYP3GuMiXaLjnWh2kzBSfIRQyNbTThnhSu3nDjAVkPexsSrPyiKimFuNgDfkGe
9+
5dQKa9Ag2SuVU4vd9SYxOMAiIFIC4ts4MLWWJf5D/PehdSuc0e5Me+91Nnbz90nl
10+
ds4lHvuDR+aKnZlTHmch3wfhXv7lNQImIBzfwl36Kd/bWB0fAEVFse3iZWmigaI/
11+
9FKh//WIq43TNLxn68OCQoyMe/HGjZDR/Xwo3rE6jg6/iAwSWib9yabfYPKbqq2G
12+
oFy6aYmmEquaDgLuX7kCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA2AZrD9cTQXTW
13+
4j2tT8N/TTc6WK2ncN4h22NTte6vK7MVwsZJCtw5ndYkmxcWkXAqiclzWyMdayds
14+
WOa12CEH7jKAhivF4Hcw3oO3JHM5BA6KzLWBVz9uZksOM6mPqn29DTKvA/Y1V8tj
15+
mxK/KUA68h/u6inu3mo4ywBpb/tqHxxg2cjyR0faCmM0pwRM0HBr/16fUMfO83nj
16+
QG8g9J/bybu5sYso/aSoC5nUNp4XjmDMdVLdqg/nTe/ejS8IfFr0WQxBlqooqFgx
17+
MSE+kX2e2fHsuOWSU/9eClt6FpQrwoC2C8F+/4g1Uz7Liqc4yMHPwjgeP9ewrrLO
18+
iIhlNNPqpQ==
19+
-----END CERTIFICATE-----
20+
-----BEGIN CERTIFICATE-----
21+
MIIDFDCCAfygAwIBAgIIICEDAxQSBwAwDQYJKoZIhvcNAQELBQAwQDE+MDwGA1UE
22+
Aww1VGVzdCByb290IENBIGZvciBQb3N0Z3JlU1FMIFNTTCByZWdyZXNzaW9uIHRl
23+
c3Qgc3VpdGUwHhcNMjEwMzAzMjIxMjA3WhcNNDgwNzE5MjIxMjA3WjBCMUAwPgYD
24+
VQQDDDdUZXN0IENBIGZvciBQb3N0Z3JlU1FMIFNTTCByZWdyZXNzaW9uIHRlc3Qg
25+
c2VydmVyIGNlcnRzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4kp2
26+
GW5nPb6QrSrtbClfZeutyQnHrm4TMPBoNepFdIVxBX/04BguM5ImDRze/huOWA+z
27+
atJAQqt3R7dsDwnOnPKUKCOuHX6a1aj5L86HtVgaWTXrZFE5NtL9PIzXkWu13UW0
28+
UesHtbPVRv6a6fB7Npph6hHy7iPZb009A8/lTJnxSPC39u/K/sPqjrVZaAJF+wDs
29+
qCxCZTUtAUFvWFnR/TeXLWlFzBupS1djgI7PltbJqSn6SKTAgNZTxpRJbu9Icp6J
30+
/50ELwT++0n0KWVXNHrDNfI5Gaa+SxClAsPsei2jLKpgR6QFC3wn38Z9q9LjAVuC
31+
+FWhoN1uhYeoricEXwIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB
32+
CwUAA4IBAQCdCA/EoXrustoV4jJGbkdXDuOUkBurwggSNBAqUBSDvCohRoD77Ecb
33+
QVuzPNxWKG+E4PwfUq2ha+2yPONEJ28ZgsbHq5qlJDMJ43wlcjn6wmmAJNeSpO8F
34+
0V9d2X/4wNZty9/zbwTnw26KChgDHumQ0WIbCoBtdqy8KDswYOvpgws6dqc021I7
35+
UrFo6vZek7VoApbJgkDL6qYADa6ApfW43ThH4sViFITeYt/kSHgmy2Udhs34jMM8
36+
xsFP/uYpRi1b1glenwSIKiHjD4/C9vnWQt5K3gRBvYukEj2Bw9VkNRpBVCi0cOoA
37+
OuwX3bwzNYNbZQv4K66oRpvuoEjCNeHg
38+
-----END CERTIFICATE-----

src/test/ssl/sslfiles.mk

+5-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ COMBINATIONS := \
6161
ssl/root+server.crl \
6262
ssl/root+client_ca.crt \
6363
ssl/root+client.crl \
64-
ssl/client+client_ca.crt
64+
ssl/client+client_ca.crt \
65+
ssl/server-cn-only+server_ca.crt
6566

6667
CERTIFICATES := root_ca server_ca client_ca $(SERVERS) $(CLIENTS)
6768
STANDARD_CERTS := $(CERTIFICATES:%=ssl/%.crt)
@@ -150,6 +151,9 @@ ssl/root+client_ca.crt: ssl/root_ca.crt ssl/client_ca.crt
150151
# and for the client, to present to the server
151152
ssl/client+client_ca.crt: ssl/client.crt ssl/client_ca.crt
152153

154+
# for the server, to present to a client that only knows the root
155+
ssl/server-cn-only+server_ca.crt: ssl/server-cn-only.crt ssl/server_ca.crt
156+
153157
# If a CRL is used, OpenSSL requires a CRL file for *all* the CAs in the
154158
# chain, even if some of them are empty.
155159
ssl/root+server.crl: ssl/root.crl ssl/server.crl

src/test/ssl/t/001_ssltests.pl

+43
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ sub switch_server_cert
3333
{
3434
$ssl_server->switch_server_cert(@_);
3535
}
36+
37+
# Determine whether this build uses OpenSSL or LibreSSL. As a heuristic, the
38+
# HAVE_SSL_CTX_SET_CERT_CB macro isn't defined for LibreSSL. (Nor for OpenSSL
39+
# 1.0.1, but that's old enough that accommodating it isn't worth the cost.)
40+
my $libressl = not check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
41+
3642
#### Some configuration
3743

3844
# This is the hostname used to connect to the server. This cannot be a
@@ -461,6 +467,43 @@ sub switch_server_cert
461467
expected_stderr =>
462468
qr/could not get server's host name from server certificate/);
463469

470+
# Test system trusted roots.
471+
switch_server_cert($node,
472+
certfile => 'server-cn-only+server_ca',
473+
keyfile => 'server-cn-only',
474+
cafile => 'root_ca');
475+
$common_connstr =
476+
"$default_ssl_connstr user=ssltestuser dbname=trustdb sslrootcert=system hostaddr=$SERVERHOSTADDR";
477+
478+
# By default our custom-CA-signed certificate should not be trusted.
479+
$node->connect_fails(
480+
"$common_connstr sslmode=verify-full host=common-name.pg-ssltest.test",
481+
"sslrootcert=system does not connect with private CA",
482+
expected_stderr => qr/SSL error: certificate verify failed/);
483+
484+
# Modes other than verify-full cannot be mixed with sslrootcert=system.
485+
$node->connect_fails(
486+
"$common_connstr sslmode=verify-ca host=common-name.pg-ssltest.test",
487+
"sslrootcert=system only accepts sslmode=verify-full",
488+
expected_stderr => qr/weak sslmode "verify-ca" may not be used with sslrootcert=system/);
489+
490+
SKIP:
491+
{
492+
skip "SSL_CERT_FILE is not supported with LibreSSL" if $libressl;
493+
494+
# We can modify the definition of "system" to get it trusted again.
495+
local $ENV{SSL_CERT_FILE} = $node->data_dir . "/root_ca.crt";
496+
$node->connect_ok(
497+
"$common_connstr sslmode=verify-full host=common-name.pg-ssltest.test",
498+
"sslrootcert=system connects with overridden SSL_CERT_FILE");
499+
500+
# verify-full mode should be the default for system CAs.
501+
$node->connect_fails(
502+
"$common_connstr host=common-name.pg-ssltest.test.bad",
503+
"sslrootcert=system defaults to sslmode=verify-full",
504+
expected_stderr => qr/server certificate for "common-name.pg-ssltest.test" does not match host name "common-name.pg-ssltest.test.bad"/);
505+
}
506+
464507
# Test that the CRL works
465508
switch_server_cert($node, certfile => 'server-revoked');
466509

0 commit comments

Comments
 (0)