On Mon, Jul 30, 2018 at 02:20:43PM +0200, Julian Markwort wrote:
> On 07/19/2018 03:00 AM, Thomas Munro wrote:
> >Some more comments:
> >
> > if (parsedline->auth_method == uaCert)
> > {
> >- parsedline->clientcert = true;
> >+ parsedline->clientcert = clientCertOn;
> > }
> >
> >The "cert" method is technically redundant with this patch, because
> >it's equivalent to "trust clientcert=verify-full", right? That gave
> >me an idea: wouldn't the control flow be a bit nicer if you just
> >treated uaCert the same as uaTrust in the big switch statement, but
> >also enforced that clientcert = clientCertFull in the hunk above?
> >Then you could just have one common code path to reach CheckCertAuth()
> >for all auth methods after that switch statement, instead of the more
> >complicated conditional coding you have now with two ways to reach it.
> >Would that be better?
> That would result in a couple less LOC and a bit clearer conditionals, I
> agree.
> If there are no objections to make uaCert a quasi-alias of uaTrust with
> clientcert=verify-full, I'll go ahead and change the code accordingly.
> I'll make sure that the error messages are still handled based on the auth
> method and not only depending on the clientcert flag.
>
> >In the "auth-cert" section there are already a couple of examples
> >using lower case "cn":
> >
> > will be sent to the client. The <literal>cn</literal> (Common Name)
> >
> > is a check that the <literal>cn</literal> attribute matches the database
> >
> >I guess you should do the same, or if there is a good reason to use
> >upper case "CN" instead then you should change those to match.
> I see that there are currently no places that use <literal>CN</literal>
> right now.
> However, we refer to Certification Authority as CA in most cases (without
> the literal tag surrounding it).
> The different fields of certificates seem to be abbreviated with capital
> letters in most literature; That was my reasoning for capitalizing it in the
> first place.
> I'm open for suggestions, but in absence of objections I might just
> capitalize all occurrences of CN.
>
> >There is still a place in the documentation where we refer to "1":
> >
> > In a <filename>pg_hba.conf</filename> record specifying certificate
> > authentication, the authentication option <literal>clientcert</literal>
> > is
> > assumed to be <literal>1</literal>, and it cannot be turned off
> >since a client
> > certificate is necessary for this method. What the
> > <literal>cert</literal>
> > method adds to the basic <literal>clientcert</literal> certificate
> >validity test
> > is a check that the <literal>cn</literal> attribute matches the database
> > user name.
> >
> >Maybe we should use "verify-ca" here, though as I noted above I think
> >it should really "verify-full" and we should simplify the flow.
> Yes, we should adopt the new style in all places.
> I'll rewrite that passage to indicate that cert authentication essentially
> results in the same behaviour as clientcert=verify-full.
> The existing text is somewhat ambiguous anyway, in case somebody decides to
> skip over the restriction described in the second sentence.
>
> >I think we should also document that "1" and "0" are older spellings
> >still accepted, where the setting names are introduced.
> >
> >+typedef enum ClientCertMode
> >+{
> >+ clientCertOff,
> >+ clientCertOn,
> >+ clientCertFull
> >+} ClientCertMode;
> >+
> +1
> >What do you think about using clientCertCA for the enumerator name
> >instead of clientCertOn? That would correspond better to the names
> >"verify-ca" and "verify-full".
> >
> +1
> I'm not sure if Magnus had any other cases in mind when he named it
> clientCertOn?
>
>
> Should I mark this entry as "Needs review" in the commitfest? It seems some
> discussion is still needed to get this commited...
I've rebased the patch atop master so it applies and passes 'make
check-world'. I didn't make any other changes.
Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778
Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate
>From 4dc30c3af69ced946b2ceab8c6b8e003d3583a00 Mon Sep 17 00:00:00 2001
From: David Fetter <[email protected]>
Date: Thu, 2 Aug 2018 23:07:29 -0700
Subject: [PATCH] clientcert_verify_full v06
To: [email protected]
---
doc/src/sgml/client-auth.sgml | 15 +++++++---
doc/src/sgml/runtime.sgml | 51 +++++++++++++++++++++++++++-------
src/backend/libpq/auth.c | 39 ++++++++++++++++++++++++--
src/backend/libpq/hba.c | 28 ++++++++++++++-----
src/include/libpq/hba.h | 9 +++++-
src/test/ssl/ServerSetup.pm | 10 ++++++-
src/test/ssl/t/001_ssltests.pl | 21 ++++++++++++++
7 files changed, 147 insertions(+), 26 deletions(-)
mode change 100644 => 100755 src/test/ssl/t/001_ssltests.pl
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index c2114021c3..6b261aea92 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -563,10 +563,17 @@ hostnossl <replaceable>database</replaceable> <replaceable>user</replaceable>
<para>
In addition to the method-specific options listed below, there is one
method-independent authentication option <literal>clientcert</literal>, which
- can be specified in any <literal>hostssl</literal> record. When set
- to <literal>1</literal>, this option requires the client to present a valid
- (trusted) SSL certificate, in addition to the other requirements of the
- authentication method.
+ can be specified in any <literal>hostssl</literal> record.
+ This option can be set to <literal>verify-ca</literal> or
+ <literal>verify-full</literal>. Both options require the client
+ to present a valid (trusted) SSL certificate, while
+ <literal>verify-full</literal> additionally enforces that the
+ <literal>CN</literal> (Common Name) in the certificate matches
+ the username or an applicable mapping.
+ This behavior is similar to the cert authentication method
+ (see <xref linkend="auth-cert"/> ) but enables pairing
+ the verification of client certificates with any authentication
+ method that supports <literal>hostssl</literal> entries.
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 06c0ee7de2..ff6493d70e 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2287,13 +2287,25 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
(<acronym>CA</acronym>s) you trust in a file in the data
directory, set the parameter <xref linkend="guc-ssl-ca-file"/> in
<filename>postgresql.conf</filename> to the new file name, and add the
- authentication option <literal>clientcert=1</literal> to the appropriate
+ authentication option <literal>clientcert=verify-ca</literal> or
+ <literal>clientcert=verify-full</literal> to the appropriate
<literal>hostssl</literal> line(s) in <filename>pg_hba.conf</filename>.
A certificate will then be requested from the client during SSL
connection startup. (See <xref linkend="libpq-ssl"/> for a description
- of how to set up certificates on the client.) The server will
- verify that the client's certificate is signed by one of the trusted
- certificate authorities.
+ of how to set up certificates on the client.)
+ </para>
+
+ <para>
+ For a <literal>hostssl</literal> entry with
+ <literal>clientcert=verify-ca</literal>, the server will verify
+ that the client's certificate is signed by one of the trusted
+ certificate authorities. If <literal>clientcert=verify-full</literal>
+ is specified, the server will not only verify the certificate
+ chain, but it will also check whether the username or its mapping
+ matches the <literal>CN</literal> (Common Name) of the provided certificate.
+ Note that certificate chain validation is always ensured when the
+ <literal>cert</literal> authentication method is used
+ (see <xref linkend="auth-cert"/>).
</para>
<para>
@@ -2317,14 +2329,33 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
client certificates against its CA file, if one is configured — but
it will not insist that a client certificate be presented.
</para>
+ </sect2>
+
+ <sect2 id="ssl-enforcing-client-certificates">
+ <title>Enforcing Client Certificates</title>
+ <para>
+ There are two approaches to enforce that users provide a certificate during login.
+ </para>
+
+ <para>
+ The first approach makes use of the <literal>cert</literal> authentication
+ method for <literal>hostssl</literal> entries in <filename>pg_hba.conf</filename>,
+ such that the certificate itself is used for authentication while also
+ providing ssl connection security. See <xref linkend="auth-cert"/> for details.
+ (It is not necessary to specify any <literal>clientcert</literal> options
+ explicitly when using the <literal>cert</literal> authentication method.)
+ In this case, the <literal>CN</literal> (Common Name) provided in
+ the certificate is checked against the user name or an applicable mapping.
+ </para>
<para>
- If you are setting up client certificates, you may wish to use
- the <literal>cert</literal> authentication method, so that the certificates
- control user authentication as well as providing connection security.
- See <xref linkend="auth-cert"/> for details. (It is not necessary to
- specify <literal>clientcert=1</literal> explicitly when using
- the <literal>cert</literal> authentication method.)
+ The second approach combines any authentication method for <literal>hostssl</literal>
+ entries with the verification of client certificates by setting the
+ <literal>clientcert</literal> authentication option to <literal>verify-ca</literal>
+ or <literal>verify-full</literal>. The former option only enforces that
+ the certificate is valid, while the latter also ensures that the
+ <literal>CN</literal> (Common Name) in the certificate matches
+ the user name or an applicable mapping.
</para>
</sect2>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 63f37902e6..0244a21f38 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -347,6 +347,7 @@ void
ClientAuthentication(Port *port)
{
int status = STATUS_ERROR;
+ int status_verify_cert_full = STATUS_ERROR;
char *logdetail = NULL;
/*
@@ -364,7 +365,7 @@ ClientAuthentication(Port *port)
* current connection, so perform any verifications based on the hba
* options field that should be done *before* the authentication here.
*/
- if (port->hba->clientcert)
+ if (port->hba->clientcert != clientCertOff)
{
/* If we haven't loaded a root certificate store, fail */
if (!secure_loaded_verify_locations())
@@ -600,10 +601,27 @@ ClientAuthentication(Port *port)
break;
}
+ if(port->hba->clientcert == clientCertFull && port->hba->auth_method!=uaCert)
+ {
+ /**
+ * Make sure we only check the Certificate if it hasn't been checked
+ * already due to the use of the uaCert authentication method.
+ */
+#ifdef USE_SSL
+ status_verify_cert_full = CheckCertAuth(port);
+#else
+ Assert(false);
+#endif
+ }
+ else
+ {
+ status_verify_cert_full = STATUS_OK;
+ }
+
if (ClientAuthentication_hook)
(*ClientAuthentication_hook) (port, status);
- if (status == STATUS_OK)
+ if (status == STATUS_OK && status_verify_cert_full == STATUS_OK)
sendAuthRequest(port, AUTH_REQ_OK, NULL, 0);
else
auth_failed(port, status, logdetail);
@@ -2758,6 +2776,7 @@ errdetail_for_ldap(LDAP *ldap)
static int
CheckCertAuth(Port *port)
{
+ int status_check_usermap = STATUS_ERROR;
Assert(port->ssl);
/* Make sure we have received a username in the certificate */
@@ -2771,7 +2790,21 @@ CheckCertAuth(Port *port)
}
/* Just pass the certificate CN to the usermap check */
- return check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
+ status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
+ if (status_check_usermap != STATUS_OK)
+ {
+ /*
+ * If clientcert=verify-full was specified and the authentication method
+ * is other than uaCert, log the reason for rejecting the authentication.
+ */
+ if (port->hba->clientcert == clientCertFull && port->hba->auth_method!=uaCert)
+ {
+ ereport(LOG,
+ (errmsg("certificate validation (clientcert=verify-full) failed for user \"%s\": CN mismatch",
+ port->user_name)));
+ }
+ }
+ return status_check_usermap;
}
#endif
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 1a65ec87bd..2c49b21989 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1609,7 +1609,7 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
*/
if (parsedline->auth_method == uaCert)
{
- parsedline->clientcert = true;
+ parsedline->clientcert = clientCertOn;
}
return parsedline;
@@ -1675,11 +1675,16 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
*err_msg = "clientcert can only be configured for \"hostssl\" rows";
return false;
}
- if (strcmp(val, "1") == 0)
+ if (strcmp(val, "1") == 0
+ || strcmp(val, "verify-ca") == 0)
{
- hbaline->clientcert = true;
+ hbaline->clientcert = clientCertOn;
}
- else
+ else if(strcmp(val, "verify-full") == 0)
+ {
+ hbaline->clientcert = clientCertFull;
+ }
+ else if (strcmp(val, "0") == 0)
{
if (hbaline->auth_method == uaCert)
{
@@ -1691,7 +1696,16 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
*err_msg = "clientcert can not be set to 0 when using \"cert\" authentication";
return false;
}
- hbaline->clientcert = false;
+ hbaline->clientcert = clientCertOff;
+ }
+ else
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("invalid value for clientcert: \"%s\"", val),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ return false;
}
}
else if (strcmp(name, "pamservice") == 0)
@@ -2250,9 +2264,9 @@ gethba_options(HbaLine *hba)
options[noptions++] =
CStringGetTextDatum(psprintf("map=%s", hba->usermap));
- if (hba->clientcert)
+ if (hba->clientcert != clientCertOff)
options[noptions++] =
- CStringGetTextDatum("clientcert=true");
+ CStringGetTextDatum(psprintf("clientcert=%s", (hba->clientcert == clientCertOn) ? "verify-ca" : "verify-full"));
if (hba->pamservice)
options[noptions++] =
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 5f68f4c666..ce9a692b8d 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -58,6 +58,13 @@ typedef enum ConnType
ctHostNoSSL
} ConnType;
+typedef enum ClientCertMode
+{
+ clientCertOff,
+ clientCertOn,
+ clientCertFull
+} ClientCertMode;
+
typedef struct HbaLine
{
int linenumber;
@@ -86,7 +93,7 @@ typedef struct HbaLine
int ldapscope;
char *ldapprefix;
char *ldapsuffix;
- bool clientcert;
+ ClientCertMode clientcert;
char *krb_realm;
bool include_realm;
bool compat_realm;
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index 3b451a360a..7738ac3f1b 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -93,8 +93,10 @@ sub configure_test_server_for_ssl
# Create test users and databases
$node->psql('postgres', "CREATE USER ssltestuser");
$node->psql('postgres', "CREATE USER anotheruser");
+ $node->psql('postgres', "CREATE USER yetanotheruser");
$node->psql('postgres', "CREATE DATABASE trustdb");
$node->psql('postgres', "CREATE DATABASE certdb");
+ $node->psql('postgres', "CREATE DATABASE verifydb");
# Update password of each user as needed.
if (defined($password))
@@ -173,11 +175,17 @@ sub configure_hba_for_ssl
# When connecting to certdb, also check the client certificate.
open my $hba, '>', "$pgdata/pg_hba.conf";
print $hba
- "# TYPE DATABASE USER ADDRESS METHOD\n";
+ "# TYPE DATABASE USER ADDRESS METHOD OPTIONS\n";
print $hba
"hostssl trustdb all $serverhost/32 $authmethod\n";
print $hba
"hostssl trustdb all ::1/128 $authmethod\n";
+ print $hba
+ "hostssl verifydb ssltestuser $serverhost/32 $authmethod clientcert=verify-full\n";
+ print $hba
+ "hostssl verifydb anotheruser $serverhost/32 $authmethod clientcert=verify-full\n";
+ print $hba
+ "hostssl verifydb yetanotheruser $serverhost/32 trust clientcert=verify-ca\n";
print $hba
"hostssl certdb all $serverhost/32 cert\n";
print $hba
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
old mode 100644
new mode 100755
index 2b875a3c95..2331dc7508
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -358,6 +358,27 @@ switch_server_cert($node, 'server-cn-only', 'root_ca');
$common_connstr =
"user=ssltestuser dbname=certdb sslkey=ssl/client_tmp.key sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
+# Check that connecting with auth-optionverify-full in pg_hba :
+# works, iff username matches Common Name
+# fails, iff username doesn't match Common Name.
+$common_connstr =
+"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
+
+test_connect_ok($common_connstr,
+ "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
+ "auth_option clientcert=verify-full succeeds with matching username and Common Name");
+
+test_connect_fails($common_connstr,
+ "user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
+ qr/FATAL/,
+ "auth_option clientcert=verify-full fails with mismatching username and Common Name");
+
+# Check that connecting with auth-optionverify-ca in pg_hba :
+# works, when username doesn't match Common Name
+test_connect_ok($common_connstr,
+ "user=yetanotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
+ "auth_option clientcert=verify-ca succeeds with mismatching username and Common Name");
+
test_connect_ok(
$common_connstr,
"sslmode=require sslcert=ssl/client+client_ca.crt",
--
2.17.1