On Thu, Feb 27, 2025 at 5:38 AM Daniel Gustafsson <dan...@yesql.se> wrote:
> Thanks for the tests, they did in fact uncover a bug in how fallback was
> handled which is now fixed.  In doing so I revamped how the default context
> handling is done, it now always use the GUCs in postgresql.conf for
> consistency.  The attached v6 rebase contains this as well as your tests as
> well as general cleanup and comment writing etc.

Great, thanks!

Revisiting my concerns from upthread:

On Thu, Jul 25, 2024 at 10:51 AM Jacob Champion
<jacob.champ...@enterprisedb.com> wrote:
> I tried patching all that, but I continue to see nondeterministic
> behavior, including the wrong certificate chain occasionally being
> served, and the servername callback being called twice for each
> connection (?!).

1) The wrong chain being served was due to the fallback bug, now fixed.
2) The servername callback happening twice is due to the TLS 1.3
HelloRetryRequest problem with our ssl_groups (which reminded me to
ping that thread [1]). Switching to TLSv1.2 in order to more easily
see the handshake on the wire makes the problem go away, which
probably did not help my sense of growing insanity last July.

>     https://github.com/openssl/openssl/issues/6109
>
> Matt Caswell appears to be convinced that SSL_set_SSL_CTX() is
> fundamentally broken.

We briefly talked about this in Brussels, and I've been trying to find
proof. Attached are some (very rough) tests that might highlight an
issue.

Basically, the new tests set up three hosts in pg_hosts.conf: one with
no client CA, one with a valid client CA, and one with a
malfunctioning CA (root+server_ca, which can't verify our client
certs). Then it switches out the default CA underneath to make sure it
does not affect the visible behavior, since that CA should not
actually be used in the end.

Unfortunately, the failure modes change depending on the default CA.
If it's not a bug in my tests, I think this may be an indication that
SSL_set_SSL_CTX() isn't fully switching out the client verification
behavior? For example, if the default CA isn't set, the other hosts
don't appear to ask for a client certificate even if they need one.
And vice versa.

--

> +           /*
> +            * Set flag to remember whether CA store has been loaded into this
> +            * SSL_context.
> +            */
> +           if (host->ssl_ca)

I think this should be `if (host->ssl_ca[0])`  -- which, incidentally,
fixes one of the new failing tests on my machine.

>  int
>  be_tls_init(bool isServerStart)
> +{
> +   SSL_CTX    *ctx;
> +   List       *sni_hosts = NIL;
> +   HostsLine   line;

A pointer to `line` is passed down to ssl_init_context(), but it's
only been partially initialized on the stack. Can it be
zero-initialized here instead?

> +       if (ssl_snimode == SSL_SNIMODE_STRICT)
> +       {
> +           ereport(COMMERROR,
> +                   (errcode(ERRCODE_PROTOCOL_VIOLATION),
> +                    errmsg("no hostname provided in callback")));
> +           return SSL_TLSEXT_ERR_ALERT_FATAL;
> +       }

At the moment we're sending an `unrecognized_name` alert in strict
mode if the client doesn't send SNI. RFC 8446 suggests
`missing_extension`:

   Additionally, all implementations MUST support the use of the
   "server_name" extension with applications capable of using it.
   Servers MAY require clients to send a valid "server_name" extension.
   Servers requiring this extension SHOULD respond to a ClientHello
   lacking a "server_name" extension by terminating the connection with
   a "missing_extension" alert.

Should we do that, or should we ignore the suggestion? The problem
with missing_extension, IMO, is that there's absolutely no indication
to the client as to which extension is missing. unrecognized_name is a
little confusing in this case (there was no name sent), but at least
the end user will be able to link that to an SNI problem via search
engine.

> +#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
> +                   # (change requires restart)

Nitpickiest nitpick: looks like the other lines use a tab instead of a
space between the setting and the trailing comment.

Thanks,
--Jacob

[1] 
https://postgr.es/m/CAOYmi%2BnTwu7%3DaUGCkf6L-ULqS8itNP7uc9nUmNLOvbXf2TCgBA%40mail.gmail.com
commit a4d9cbf4d1228dcc17f2961b7811321a50e74617
Author: Jacob Champion <jacob.champ...@enterprisedb.com>
Date:   Tue Mar 4 13:17:12 2025 -0800

    Tests

diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
index f0ce048273a..72e64c6c00d 100644
--- a/src/test/ssl/t/004_sni.pl
+++ b/src/test/ssl/t/004_sni.pl
@@ -33,6 +33,11 @@ if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
 
 my $ssl_server = SSL::Server->new();
 
+sub sslkey
+{
+       return $ssl_server->sslkey(@_);
+}
+
 my $node = PostgreSQL::Test::Cluster->new('primary');
 $node->init;
 
@@ -161,4 +166,57 @@ $node->connect_fails(
        "connect fails since the passphrase protected key cannot be reloaded",
        expected_stderr => qr/tlsv1 unrecognized name/);
 
+# Test client CAs.
+
+$connstr =
+  "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require 
sslsni=1";
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+# example.org has an unconfigured CA.
+$node->append_conf('pg_hosts.conf', 'example.org server-cn-only.crt 
server-cn-only.key ""');
+# example.com uses the client CA.
+$node->append_conf('pg_hosts.conf', 'example.com server-cn-only.crt 
server-cn-only.key root+client_ca.crt');
+# example.net uses the server CA (which is wrong).
+$node->append_conf('pg_hosts.conf', 'example.net server-cn-only.crt 
server-cn-only.key root+server_ca.crt');
+$node->reload;
+
+my @cases = ( "", "root+client_ca", "root+server_ca" );
+foreach my $default_ca (@cases)
+{
+       # The default CA should, ideally, not matter for the purposes of these
+       # tests, since we connect to the other hosts explicitly.
+       $ssl_server->switch_server_cert(
+               $node,
+               certfile => 'server-cn-only',
+               cafile => $default_ca);
+
+       # example.org is unconfigured and should fail.
+       $node->connect_fails(
+               "$connstr host=example.org sslcertmode=require 
sslcert=ssl/client.crt " . sslkey('client.key'),
+               "example.org, $default_ca: connect with sslcert, no client CA 
configured",
+               expected_stderr => qr/client certificates can only be checked 
if a root certificate store is available/);
+
+       # example.com is configured and should require a valid client cert.
+       $node->connect_fails(
+               "$connstr host=example.com sslcertmode=disable",
+               "example.com, $default_ca: connect fails if no client 
certificate sent",
+               expected_stderr => qr/connection requires a valid client 
certificate/);
+
+       $node->connect_ok(
+               "$connstr host=example.com sslcertmode=require 
sslcert=ssl/client.crt " . sslkey('client.key'),
+               "example.com, $default_ca: connect with sslcert, client 
certificate sent");
+
+       # example.net is configured and should require a client cert, but will
+       # always fail verification.
+       $node->connect_fails(
+               "$connstr host=example.net sslcertmode=disable",
+               "example.net, $default_ca: connect fails if no client 
certificate sent",
+               expected_stderr => qr/connection requires a valid client 
certificate/);
+
+       $node->connect_fails(
+               "$connstr host=example.net sslcertmode=require 
sslcert=ssl/client.crt " . sslkey('client.key'),
+               "example.net, $default_ca: connect with sslcert, client 
certificate sent",
+               expected_stderr => qr/unknown ca/);
+}
+
 done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm 
b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index e044318531f..bdcce84003e 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -71,6 +71,7 @@ sub init
        chmod(0600, glob "$pgdata/server-*.key")
          or die "failed to change permissions on server keys: $!";
        _copy_files("ssl/root+client_ca.crt", $pgdata);
+       _copy_files("ssl/root+server_ca.crt", $pgdata);
        _copy_files("ssl/root_ca.crt", $pgdata);
        _copy_files("ssl/root+client.crl", $pgdata);
        mkdir("$pgdata/root+client-crldir")
@@ -145,7 +146,8 @@ following parameters are supported:
 =item cafile => B<value>
 
 The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
-default to 'root+client_ca.crt'.
+default to 'root+client_ca.crt'. If empty, no C<ssl_ca_file> configuration
+parameter will be set.
 
 =item certfile => B<value>
 
@@ -180,10 +182,11 @@ sub set_server_cert
          unless defined $params->{keyfile};
 
        my $sslconf =
-               "ssl_ca_file='$params->{cafile}.crt'\n"
-         . "ssl_cert_file='$params->{certfile}.crt'\n"
+               "ssl_cert_file='$params->{certfile}.crt'\n"
          . "ssl_key_file='$params->{keyfile}.key'\n"
          . "ssl_crl_file='$params->{crlfile}'\n";
+       $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n"
+         if $params->{cafile} ne "";
        $sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
          if defined $params->{crldir};
 

Reply via email to