]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
oauth: Add TLS support for oauth_validator tests
authorJacob Champion <jchampion@postgresql.org>
Thu, 5 Mar 2026 18:04:53 +0000 (10:04 -0800)
committerJacob Champion <jchampion@postgresql.org>
Thu, 5 Mar 2026 18:04:53 +0000 (10:04 -0800)
The oauth_validator tests don't currently support HTTPS, which makes
testing PGOAUTHCAFILE difficult. Add a localhost certificate to
src/test/ssl and make use of it in oauth_server.py.

In passing, explain the hardcoded use of IPv4 in our issuer identifier,
after intermittent failures on NetBSD led to commit 8d9d5843b. (The new
certificate is still set up for IPv6, to make it easier to improve that
behavior in the future.)

Patch by Jonathan Gonzalez V., with some additional tests and tweaks by
me.

Author: Jonathan Gonzalez V. <jonathan.abdiel@gmail.com>
Discussion: https://postgr.es/m/8a296a2c128aba924bff0ae48af2b88bf8f9188d.camel@gmail.com

src/test/modules/oauth_validator/Makefile
src/test/modules/oauth_validator/meson.build
src/test/modules/oauth_validator/t/001_server.pl
src/test/modules/oauth_validator/t/OAuth/Server.pm
src/test/modules/oauth_validator/t/oauth_server.py
src/test/ssl/conf/server-localhost-alt-names.config [new file with mode: 0644]
src/test/ssl/ssl/server-localhost-alt-names.crt [new file with mode: 0644]
src/test/ssl/ssl/server-localhost-alt-names.key [new file with mode: 0644]
src/test/ssl/sslfiles.mk

index cb64f0f1437077183970ac46c4cb5b88b19cdf37..0b39a88fd9fecdb4f2308e2bb5da682d561c3dc4 100644 (file)
@@ -36,5 +36,6 @@ include $(top_srcdir)/contrib/contrib-global.mk
 export PYTHON
 export with_libcurl
 export with_python
+export cert_dir=$(top_srcdir)/src/test/ssl/ssl
 
 endif
index c4b73e05297ed6837a2ddc846a50ab09d8b0b550..506a9894b8d3cc9f7164c66efddd53da8160b3f3 100644 (file)
@@ -80,6 +80,7 @@ tests += {
       'PYTHON': python.full_path(),
       'with_libcurl': oauth_flow_supported ? 'yes' : 'no',
       'with_python': 'yes',
+      'cert_dir': meson.project_source_root() / 'src/test/ssl/ssl',
     },
     'deps': [oauth_hook_client],
   },
index 6b649c0b06fe21e8d5b79f1bb1d151679f866251..cdad2ae8011c24493f545eecdeab9d9688486dad 100644 (file)
@@ -71,9 +71,31 @@ 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.
 my $port = $webserver->port();
 my $issuer = "http://127.0.0.1:$port";
 
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf(
+       'pg_hba.conf', qq{
+local all test oauth issuer="$issuer" scope="openid postgres"
+});
+$node->reload;
+
+my $log_start = $node->wait_for_log(qr/reloading configuration files/);
+
+$node->connect_fails(
+       "user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
+       "HTTPS is required without debug mode",
+       expected_stderr =>
+         qr@OAuth discovery URI "\Q$issuer\E/.well-known/openid-configuration" must use HTTPS@
+);
+
+# Switch to HTTPS.
+$issuer = "https://127.0.0.1:$port";
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf(
        'pg_hba.conf', qq{
@@ -83,7 +105,8 @@ local all testparam oauth issuer="$issuer/param" scope="openid postgres"
 });
 $node->reload;
 
-my $log_start = $node->wait_for_log(qr/reloading configuration files/);
+$log_start =
+  $node->wait_for_log(qr/reloading configuration files/, $log_start);
 
 # Check pg_hba_file_rules() support.
 my $contents = $bgconn->query_safe(
@@ -96,16 +119,26 @@ is( $contents,
 3|oauth|\{issuer=$issuer/param,"scope=openid postgres",validator=validator\}},
        "pg_hba_file_rules recreates OAuth HBA settings");
 
-# To test against HTTP rather than HTTPS, we need to enable PGOAUTHDEBUG. But
-# first, check to make sure the client refuses such connections by default.
+# 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 is required without debug mode",
-       expected_stderr =>
-         qr@OAuth discovery URI "\Q$issuer\E/.well-known/openid-configuration" must use HTTPS@
-);
-
-$ENV{PGOAUTHDEBUG} = "UNSAFE";
+       "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";
 
 my $user = "test";
 $node->connect_ok(
index 89ea7ab4a4c26b24f97207d8070ba31321cd7f83..d923d4c5eb2ecd1ab8e4bd7942ba9be026d7fdff 100644 (file)
@@ -15,7 +15,7 @@ OAuth::Server - runs a mock OAuth authorization server for testing
   $server->run;
 
   my $port = $server->port;
-  my $issuer = "http://127.0.0.1:$port";
+  my $issuer = "https://127.0.0.1:$port";
 
   # test against $issuer...
 
@@ -27,9 +27,8 @@ This is glue API between the Perl tests and the Python authorization server
 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 does not use TLS (it implements a nonstandard, unsafe
-issuer at "http://127.0.0.1:<port>"), so libpq in particular will need to set
-PGOAUTHDEBUG=UNSAFE to be able to talk to it.
+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.
 
 =cut
 
index c70783ecbe49d4ab0cc945021e9507040b564bb8..6df8c2ca5df3b4d9db39a136059874fa0949a5d7 100755 (executable)
@@ -11,12 +11,17 @@ import functools
 import http.server
 import json
 import os
+import ssl
 import sys
 import time
 import urllib.parse
 from collections import defaultdict
 from typing import Dict
 
+ssl_dir = os.getenv("cert_dir")
+ssl_cert = ssl_dir + "/server-localhost-alt-names.crt"
+ssl_key = ssl_dir + "/server-localhost-alt-names.key"
+
 
 class OAuthHandler(http.server.BaseHTTPRequestHandler):
     """
@@ -295,7 +300,11 @@ class OAuthHandler(http.server.BaseHTTPRequestHandler):
     def config(self) -> JsonObject:
         port = self.server.socket.getsockname()[1]
 
-        issuer = f"http://127.0.0.1:{port}"
+        # XXX This IPv4-only Issuer can't be changed to "localhost" unless our
+        # server also listens on the corresponding IPv6 port when available.
+        # Otherwise, other processes with ephemeral sockets could accidentally
+        # interfere with our Curl client, causing intermittent failures.
+        issuer = f"https://127.0.0.1:{port}"
         if self._alt_issuer:
             issuer += "/alternate"
         elif self._parameterized:
@@ -408,9 +417,18 @@ def main():
     Starts the authorization server on localhost. The ephemeral port in use will
     be printed to stdout.
     """
-
+    # XXX Listen exclusively on IPv4. Listening on a dual-stack socket would be
+    # more true-to-life, but every OS/Python combination in the buildfarm and CI
+    # would need to provide the functionality first.
     s = http.server.HTTPServer(("127.0.0.1", 0), OAuthHandler)
 
+    # Speak HTTPS.
+    # TODO: switch to HTTPSServer with Python 3.14
+    ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    ssl_context.load_cert_chain(ssl_cert, ssl_key)
+
+    s.socket = ssl_context.wrap_socket(s.socket, server_side=True)
+
     # Attach a "cache" dictionary to the server to allow the OAuthHandlers to
     # track state across token requests. The use of defaultdict ensures that new
     # entries will be created automatically.
diff --git a/src/test/ssl/conf/server-localhost-alt-names.config b/src/test/ssl/conf/server-localhost-alt-names.config
new file mode 100644 (file)
index 0000000..1c41c1e
--- /dev/null
@@ -0,0 +1,20 @@
+# An OpenSSL format CSR config file for creating a server certificate.
+#
+# This certificate contains SANs for localhost (DNS, IPv4, and IPv6).
+
+[ req ]
+distinguished_name     = req_distinguished_name
+req_extensions         = v3_req
+prompt                 = no
+
+[ req_distinguished_name ]
+OU = PostgreSQL test suite
+
+# For Subject Alternative Names
+[ v3_req ]
+subjectAltName = @alt_names
+
+[ alt_names ]
+DNS.1 = localhost
+IP.1 = 127.0.0.1
+IP.2 = ::1
diff --git a/src/test/ssl/ssl/server-localhost-alt-names.crt b/src/test/ssl/ssl/server-localhost-alt-names.crt
new file mode 100644 (file)
index 0000000..106dc34
--- /dev/null
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDVjCCAj6gAwIBAgIIICYCJxRTBwAwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UE
+Aww3VGVzdCBDQSBmb3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IHNl
+cnZlciBjZXJ0czAgFw0yNjAyMjcyMjUzMDdaGA8yMDUzMDcxNTIyNTMwN1owIDEe
+MBwGA1UECwwVUG9zdGdyZVNRTCB0ZXN0IHN1aXRlMIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEA3k/aT/OV8sbJrvhtSgz5eNMCuv7RKdUQw+f52DpZTs85
+lTXIRs+l3mXoKRjN1gqzqlHInnJlhxQipqGiJfz4Li8L6jma2yZztFHH+f+YF8Ke
+5fCYP1qMxbghqeIRkKgrCEjHUnOhbN5oMi/Ndt9AXWGG/39uk5Xec/Y/J5aZkPVV
+blqWYyQQ+4U783lwZs1EUWdfiTVRp8fYADT/2lHjaZaX08vAE5VvCbBv6mPhPfno
+F9FIaW+CRuwORisFK8Bd1q/0r5aPZGPi0lokCdaB/cRUHwJK1/HHgyB3N+Lk4swf
+z+MfSqj4IaNPW7zn3EV9hgpVwSmB5ES8rzojiGtMDQIDAQABo3AwbjAsBgNVHREE
+JTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwHQYDVR0OBBYE
+FOZ8KClKVbeYecn8lvAldBXOjQz6MB8GA1UdIwQYMBaAFPKPOmZAUGRIItcugv9W
+nsKz7nQKMA0GCSqGSIb3DQEBCwUAA4IBAQDE1FGw20H0Flo3gAGN0ND9G/6wDxWM
+MldbXRjqc1E0/+7+Zs6v1jPrNUNEvxy5kHWevUJCIt6y4SYt01JxE4wqEPJ3UBAv
+cM0p08mohmN/CHc/lswXx12MZMfaLA1/WRPqvtiGFOrOOPvaRKHO4ORiT1KWmtOO
+FgcW9E1Q1iJFK28xdz9NEEBWEurEIr5KGAsCwf9DfQxPJXiS9n98BDI8gPwlse7t
+VqyhGVSj+EPbdY2kqkSuPXacdnUGfO6EWo9PFKqhxWMxABLuK0UZzH6/1lMOh1m9
+Mm+gtwO5RLBX22V+KIs1uuDTNcveQ2DsZnMZh7lGD05eHYG9hwnC6GNZ
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/server-localhost-alt-names.key b/src/test/ssl/ssl/server-localhost-alt-names.key
new file mode 100644 (file)
index 0000000..4499a11
--- /dev/null
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDeT9pP85Xyxsmu
++G1KDPl40wK6/tEp1RDD5/nYOllOzzmVNchGz6XeZegpGM3WCrOqUciecmWHFCKm
+oaIl/PguLwvqOZrbJnO0Ucf5/5gXwp7l8Jg/WozFuCGp4hGQqCsISMdSc6Fs3mgy
+L81230BdYYb/f26Tld5z9j8nlpmQ9VVuWpZjJBD7hTvzeXBmzURRZ1+JNVGnx9gA
+NP/aUeNplpfTy8ATlW8JsG/qY+E9+egX0Uhpb4JG7A5GKwUrwF3Wr/Svlo9kY+LS
+WiQJ1oH9xFQfAkrX8ceDIHc34uTizB/P4x9KqPgho09bvOfcRX2GClXBKYHkRLyv
+OiOIa0wNAgMBAAECggEAFchiPkJCV4r12RCbeM2DpjyawGLWcNBhN6jjuLWi6Y9x
+d3bRHGsdOAjpMhmtlYLv7sjbrPbNjupAqO4eerVqRfAzLSyeyUlfvfPjcdIC/5UA
+x8wGxvJi576ugbxWd0ObD9E9woz07LtwHzbC3ZprbprvRNqiJZDiPp+KuaDOhD7u
+6XAM8JilFqfiDN8+xbH2dWdVkdt2OD5wctJbqy6moH9VFVsWsMQr3/vJkSdUPLxa
+8ATUubFhO/sqE+KsMZESq5W1Xbj3NwMkvnA92yG9+ED60NPjFzgheZZWSmXe1B/c
+XB3G/upvCoHEgKbrnYt05b/ryUbXAZkvi5oL4fp9OwKBgQD4d+Qm4GiKEWvjZ5II
+ROfHEyoWOHw9z8ydJIrtOL8ICh5RH8D/v2IaMAacWV5eLoJ7aYC6yIYuWdHQljAi
+zltNFrsLFmWXLy91IWfUzIGnFLWeqOmI50vlM8xU54rD/cZ3qtvr2Qk9HHs0dsyB
+6cGRf0BPJi04aAEqSZqc8HCXAwKBgQDlDP0MW57bHpqQROQDLIgEX9/rzUNo48Z/
+1f27bCkKP+CpizE9eWvGs5rQmUxCNzWULFxIuBbgsubuVP7jO3piY6bRGnvSE6nD
+mW0V1mSypVO22Ci/Q8ekkY2+0ZVp3qLPO/cwtI/Ye8kp4xu41I2XgJE8Mo0hEEyJ
+N1/1vUJbrwKBgBp3gukVPG2An5JwpOCWnm3ZP8FwMOPQr8YJb3cHdWng0gvoKwHT
+HBsYBIxBBMlZgPKucVT0KT7kuHHUnboHazhR9Iig0R+CmjaK4WmMgz8N+K625XF8
+2dvHYbulkmWAMdTrcVO1IcPNtd4HzY8FHGZoPKxxr51zjrQ3dO3EuumLAoGATho2
+sx8OtPLji2wiP77QhoVWqmYspTh9+Bs00NLZz6fmaImQ+cBMcs3NbXHIYg/HUkYq
+FZXIH0iBnCUZYMxoN+J5AHZCYGjaC1tmqfqYDZ54RDHC+y0Wh1QmfDmk9Bu5cmal
+LFN1dUEIYCMT0duQiGeLnnYyT2LqZiOesgGd/fsCgYEA2GbKteq+io6HAEt2/yry
+xZGaRR8Twg0B8XtD9NHCbgizmZiD/mADgyhkgjUsDIkcMzEt+sA4IK9ORgIYqS+/
+q2eY1QRKpoZgJJfE8dU88B35YGqdZuXENR4I7w+JrKCCCk5jSiwylvsBsi1HX8Qu
+EdQBBRiwkRnxQ83hqRI3ymw=
+-----END PRIVATE KEY-----
index ecb40588c878e558c8af8c4c6aee671d8cabd4f7..f32c53a76a1381d72c4632c96667a0aa553f3f75 100644 (file)
@@ -31,6 +31,7 @@ SERVERS := server-cn-and-alt-names \
        server-ip-in-dnsname \
        server-single-alt-name \
        server-multiple-alt-names \
+       server-localhost-alt-names \
        server-no-names \
        server-revoked
 CLIENTS := client client-dn client-revoked client_ext client-long \