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"; +{ + # 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); - -# Now we can use our alternative CA. -$ENV{PGOAUTHCAFILE} = "$ENV{cert_dir}/root+server_ca.crt"; + $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