export PYTHON
export with_libcurl
export with_python
+export cert_dir=$(top_srcdir)/src/test/ssl/ssl
endif
'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],
},
$? = $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{
});
$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(
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(
$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...
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
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):
"""
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:
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.
--- /dev/null
+# 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
--- /dev/null
+-----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-----
--- /dev/null
+-----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-----
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 \