From: Jacob Champion Date: Mon, 30 Mar 2026 21:14:45 +0000 (-0700) Subject: libpq: Add oauth_ca_file option to change CAs without debugging X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=993368113;p=thirdparty%2Fpostgresql.git libpq: Add oauth_ca_file option to change CAs without debugging PG18 hid the PGOAUTHCAFILE envvar behind PGOAUTHDEBUG=UNSAFE, because I thought that any "real" production usage of private CA certificates would have them added to the Curl system trust store. But there are use cases, such as containerized environments, that prefer to manage custom CA settings more granularly; some of them consider envvar configuration of certificates to be standard practice. Move PGOAUTHCAFILE out from under the debug flag, and add an oauth_ca_file option to libpq to configure trusted CAs per connection. Patch by Jonathan Gonzalez V., with some additional wordsmithing and test organization by me. Author: Jonathan Gonzalez V. Co-authored-by: Jacob Champion Reviewed-by: Zsolt Parragi Discussion: https://postgr.es/m/16a91d02795cb991963326a902afa764e4d721db.camel%40gmail.com --- diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 6db823808fc..a48d3161495 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -2585,6 +2585,23 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + + oauth_ca_file + + + The name of a file containing one or more SSL certificate authority + (CA) certificates, which will be used to verify the + identity of the authorization server and its endpoints. By default, the + Curl system certificate bundle is used. + + + This parameter does not affect verification of the + PostgreSQL server certificate; see + instead. + + + + @@ -9422,6 +9439,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) linkend="libpq-connect-max-protocol-version"/> connection parameter. + + + + + PGOAUTHCAFILE + + PGOAUTHCAFILE behaves the same as the connection parameter. + + @@ -10608,6 +10635,13 @@ typedef struct Debugging and Developer Settings + + While developing against a local authorization server, it may be helpful to + make use of the connection + parameter (or the equivalent PGOAUTHCAFILE environment + variable) in the client application. + + A "dangerous debugging mode" may be enabled by setting the environment variable PGOAUTHDEBUG=UNSAFE. This functionality is provided @@ -10620,12 +10654,6 @@ typedef struct permits the use of unencrypted HTTP during the OAuth provider exchange - - - allows the system's trusted CA list to be completely replaced using the - PGOAUTHCAFILE environment variable - - prints HTTP traffic (containing several critical secrets) to standard diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c index 052ecd32da2..3baede1b2e7 100644 --- a/src/interfaces/libpq-oauth/oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -216,6 +216,7 @@ struct async_ctx /* relevant connection options cached from the PGconn */ char *client_id; /* oauth_client_id */ char *client_secret; /* oauth_client_secret (may be NULL) */ + char *ca_file; /* oauth_ca_file */ /* options cached from the PGoauthBearerRequest (we don't own these) */ const char *discovery_uri; @@ -336,6 +337,7 @@ free_async_ctx(struct async_ctx *actx) free(actx->client_id); free(actx->client_secret); + free(actx->ca_file); free(actx); } @@ -1833,21 +1835,9 @@ setup_curl_handles(struct async_ctx *actx) CHECK_SETOPT(actx, popt, protos, return false); } - /* - * If we're in debug mode, allow the developer to change the trusted CA - * list. For now, this is not something we expose outside of the UNSAFE - * mode, because it's not clear that it's useful in production: both libpq - * and the user's browser must trust the same authorization servers for - * the flow to work at all, so any changes to the roots are likely to be - * done system-wide. - */ - if (actx->debugging) - { - const char *env; - - if ((env = getenv("PGOAUTHCAFILE")) != NULL) - CHECK_SETOPT(actx, CURLOPT_CAINFO, env, return false); - } + /* Allow the user to change the trusted CA list. */ + if (actx->ca_file != NULL) + CHECK_SETOPT(actx, CURLOPT_CAINFO, actx->ca_file, return false); /* * Suppress the Accept header to make our request as minimal as possible. @@ -3125,6 +3115,12 @@ pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request) if (!actx->client_secret) goto oom; } + else if (strcmp(opt->keyword, "oauth_ca_file") == 0) + { + actx->ca_file = strdup(opt->val); + if (!actx->ca_file) + goto oom; + } } PQconninfoFree(conninfo); diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index db9b4c8edbf..4272d386e64 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -413,6 +413,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "OAuth-Scope", "", 15, offsetof(struct pg_conn, oauth_scope)}, + {"oauth_ca_file", "PGOAUTHCAFILE", NULL, NULL, + "OAuth-CA-File", "", 64, + offsetof(struct pg_conn, oauth_ca_file)}, + {"sslkeylogfile", NULL, NULL, NULL, "SSL-Key-Log-File", "D", 64, offsetof(struct pg_conn, sslkeylogfile)}, @@ -5158,6 +5162,7 @@ freePGconn(PGconn *conn) free(conn->oauth_discovery_uri); free(conn->oauth_client_id); free(conn->oauth_client_secret); + free(conn->oauth_ca_file); free(conn->oauth_scope); /* Note that conn->Pfdebug is not ours to close or free */ free(conn->events); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index bd7eb59f5f8..23de98290c9 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -444,6 +444,7 @@ struct pg_conn char *oauth_client_secret; /* client secret */ char *oauth_scope; /* access token scope */ char *oauth_token; /* access token */ + char *oauth_ca_file; /* CA file path */ bool oauth_want_retry; /* should we retry on failure? */ /* Optional file to write trace info to */ diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl index cdad2ae8011..9e4dba8c924 100644 --- a/src/test/modules/oauth_validator/t/001_server.pl +++ b/src/test/modules/oauth_validator/t/001_server.pl @@ -71,9 +71,9 @@ END $? = $exit_code; } -# To test against HTTPS with our custom CA, we need to enable PGOAUTHDEBUG and -# PGOAUTHCAFILE. But first, check to make sure the client refuses HTTP and -# untrusted HTTPS connections by default. +# To test against HTTPS with our custom CA, we'll set PGOAUTHCAFILE. But first, +# check to make sure the client refuses HTTP and untrusted HTTPS connections by +# default. my $port = $webserver->port(); my $issuer = "http://127.0.0.1:$port"; @@ -119,28 +119,46 @@ is( $contents, 3|oauth|\{issuer=$issuer/param,"scope=openid postgres",validator=validator\}}, "pg_hba_file_rules recreates OAuth HBA settings"); -# Make sure PGOAUTHDEBUG=UNSAFE doesn't disable certificate verification. -$ENV{PGOAUTHDEBUG} = "UNSAFE"; - -$node->connect_fails( - "user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635", - "HTTPS trusts only system CA roots by default", - # Note that the latter half of this error message comes from Curl, which has - # had a few variants since 7.61: - # - # - SSL peer certificate or SSH remote key was not OK - # - Peer certificate cannot be authenticated with given CA certificates - # - Issuer check against peer certificate failed - # - # Key off of the "peer certificate" portion, since that seems to have - # remained constant over a long period of time. - expected_stderr => - qr/failed to fetch OpenID discovery document:.*peer certificate/i); - -# Now we can use our alternative CA. -$ENV{PGOAUTHCAFILE} = "$ENV{cert_dir}/root+server_ca.crt"; +{ + # Make sure PGOAUTHDEBUG=UNSAFE doesn't disable certificate verification. + local $ENV{PGOAUTHDEBUG} = "UNSAFE"; + + $node->connect_fails( + "user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635", + "HTTPS trusts only system CA roots by default", + # Note that the latter half of this error message comes from Curl, which + # has had a few variants since 7.61: + # + # - SSL peer certificate or SSH remote key was not OK + # - Peer certificate cannot be authenticated with given CA certificates + # - Issuer check against peer certificate failed + # + # Key off of the "peer certificate" portion, since that seems to have + # remained constant over a long period of time. + expected_stderr => + qr/failed to fetch OpenID discovery document:.*peer certificate/i); +} +my $alternative_ca = "$ENV{cert_dir}/root+server_ca.crt"; my $user = "test"; + +# Make sure we can use oauth_ca_file option to specify the alternative CA path +$node->connect_ok( + "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635 oauth_ca_file=$alternative_ca", + "connect as test (oauth_ca_file)", + expected_stderr => + qr@Visit https://example\.com/ and enter the code: postgresuser@, + log_like => [ + qr/oauth_validator: token="9243959234", role="$user"/, + qr/oauth_validator: issuer="\Q$issuer\E", scope="openid postgres"/, + qr/connection authenticated: identity="test" method=oauth/, + qr/connection authorized/, + ]); + +# Make sure that we can use the environment variable without PGOAUTHDEBUG, and +# then use it for the rest of the tests +$ENV{PGOAUTHCAFILE} = $alternative_ca; + $node->connect_ok( "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635", "connect as test", @@ -153,6 +171,9 @@ $node->connect_ok( qr/connection authorized/, ]); +# Enable PGOAUTHDEBUG for all remaining tests. +$ENV{PGOAUTHDEBUG} = "UNSAFE"; + # The /alternate issuer uses slightly different parameters, along with an # OAuth-style discovery document. $user = "testalt"; diff --git a/src/test/modules/oauth_validator/t/OAuth/Server.pm b/src/test/modules/oauth_validator/t/OAuth/Server.pm index d923d4c5eb2..62a29c283df 100644 --- a/src/test/modules/oauth_validator/t/OAuth/Server.pm +++ b/src/test/modules/oauth_validator/t/OAuth/Server.pm @@ -28,7 +28,7 @@ daemon implemented in t/oauth_server.py. (Python has a fairly usable HTTP server in its standard library, so the implementation was ported from Perl.) This authorization server serves HTTPS on 127.0.0.1 (IPv4 only). libpq will need -to set PGOAUTHDEBUG=UNSAFE and PGOAUTHCAFILE with the right CA. +to set PGOAUTHCAFILE with the right CA. =cut