On 3/4/21 2:16 PM, Joel Jacobson wrote:
> On Sat, Jan 30, 2021, at 22:18, Andrew Dunstan wrote:
>> ssl-match-client-cert-dn-v3.patch
>
> Spelling error of "conjunction":
> +       This option is probably best used in comjunction with a
> username map.
>
>



Yeah, fixed this, added a bit more docco, and got rid of some bitrot.


cheers


andrew



--
Andrew Dunstan
EDB: https://www.enterprisedb.com

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index b420486a0a..61b646c20a 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -598,7 +598,7 @@ hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceabl
       </para>
 
       <para>
-       In addition to the method-specific options listed below, there is one
+       In addition to the method-specific options listed below, there is a
        method-independent authentication option <literal>clientcert</literal>, which
        can be specified in any <literal>hostssl</literal> record.
        This option can be set to <literal>verify-ca</literal> or
@@ -612,6 +612,27 @@ hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceabl
        the verification of client certificates with any authentication
        method that supports <literal>hostssl</literal> entries.
       </para>
+      <para>
+       On any record using client certificate authentication (i.e. one
+       using the <literal>cert</literal> authentication method or one
+       using the <literal>clientcert</literal> option), you can specify
+       which part of the client certificate credentials to match using
+       the <literal>clientname</literal> option. This option can have one
+       of two values. If you specify <literal>clientname=CN</literal>, which
+       is the default, the username is matched against the certificate's
+       <literal>Common Name (CN)</literal>. If instead you specify
+       <literal>clientname=DN</literal> the username is matched against the
+       entire <literal>Distinguished Name (DN)</literal> of the certificate.
+       This option is probably best used in conjunction with a username map.
+       The comparison is done with the <literal>DN</literal> in RFC2253 format.
+       To see the <literal>DN</literal> of a client certificate in this format,
+       do
+<programlisting>
+openssl x509 -in myclient.crt -noout --subject -nameopt RFC2253 | sed "s/^subject=//"
+</programlisting>
+        Care need to be taken when using this option, especially when using
+        regular expression matching against the <literal>DN</literal>.
+      </para>
      </listitem>
     </varlistentry>
    </variablelist>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..5b4bad4cff 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2800,12 +2800,15 @@ static int
 CheckCertAuth(Port *port)
 {
 	int			status_check_usermap = STATUS_ERROR;
+	char	   *peer_username;
 
 	Assert(port->ssl);
 
 	/* Make sure we have received a username in the certificate */
-	if (port->peer_cn == NULL ||
-		strlen(port->peer_cn) <= 0)
+	peer_username = port->hba->clientcertname == clientCertCN ? port->peer_cn : port->peer_dn;
+
+	if (peer_username == NULL ||
+		strlen(peer_username) <= 0)
 	{
 		ereport(LOG,
 				(errmsg("certificate authentication failed for user \"%s\": client certificate contains no user name",
@@ -2813,8 +2816,8 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
-	/* Just pass the certificate cn to the usermap check */
-	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
+	/* Just pass the certificate cn/dn to the usermap check */
+	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, peer_username, false);
 	if (status_check_usermap != STATUS_OK)
 	{
 		/*
@@ -2825,7 +2828,7 @@ CheckCertAuth(Port *port)
 		if (port->hba->clientcert == clientCertFull && port->hba->auth_method != uaCert)
 		{
 			ereport(LOG,
-					(errmsg("certificate validation (clientcert=verify-full) failed for user \"%s\": CN mismatch",
+					(errmsg("certificate validation (clientcert=verify-full) failed for user \"%s\": CN/DN mismatch",
 							port->user_name)));
 		}
 	}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 4c4f025eb1..d0184a2ce2 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -543,22 +543,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -582,6 +585,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -610,6 +643,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index bb603ad209..ad0abf7058 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -121,7 +121,7 @@ secure_open_server(Port *port)
 
 	ereport(DEBUG2,
 			(errmsg_internal("SSL connection from \"%s\"",
-					port->peer_cn ? port->peer_cn : "(anonymous)")));
+					port->peer_dn ? port->peer_dn : "(anonymous)")));
 #endif
 
 	return r;
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a04c093d5..feb711a6ef 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1753,6 +1753,37 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 			return false;
 		}
 	}
+	else if (strcmp(name, "clientname") == 0)
+	{
+		if (hbaline->conntype != ctHostSSL)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("clientname can only be configured for \"hostssl\" rows"),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = "clientname can only be configured for \"hostssl\" rows";
+			return false;
+		}
+
+		if (strcmp(val, "CN") == 0)
+		{
+			hbaline->clientcertname = clientCertCN;
+		}
+		else if (strcmp(val, "DN") == 0)
+		{
+			hbaline->clientcertname = clientCertDN;
+		}
+		else
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("invalid value for clientname: \"%s\"", val),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			return false;
+		}
+	}
 	else if (strcmp(name, "pamservice") == 0)
 	{
 		REQUIRE_AUTH_OPTION(uaPAM, "pamservice", "pam");
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..443b3560ef 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -69,7 +69,13 @@ typedef enum ClientCertMode
 	clientCertOff,
 	clientCertCA,
 	clientCertFull
-} ClientCertMode;
+}			ClientCertMode;
+
+typedef enum ClientCertName
+{
+	clientCertCN,
+	clientCertDN
+}			ClientCertName;
 
 typedef struct HbaLine
 {
@@ -101,6 +107,7 @@ typedef struct HbaLine
 	char	   *ldapprefix;
 	char	   *ldapsuffix;
 	ClientCertMode clientcert;
+	ClientCertName clientcertname;
 	char	   *krb_realm;
 	bool		include_realm;
 	bool		compat_realm;
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7be1a67d69..c206a859fe 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -190,6 +190,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
diff --git a/src/test/ssl/Makefile b/src/test/ssl/Makefile
index 4b53fdf6c0..a9eca9e049 100644
--- a/src/test/ssl/Makefile
+++ b/src/test/ssl/Makefile
@@ -18,7 +18,7 @@ export with_ssl
 CERTIFICATES := server_ca server-cn-and-alt-names \
 	server-cn-only server-single-alt-name server-multiple-alt-names \
 	server-no-names server-revoked server-ss \
-	client_ca client client-revoked \
+	client_ca client client-dn client-revoked \
 	root_ca
 
 SSLFILES := $(CERTIFICATES:%=ssl/%.key) $(CERTIFICATES:%=ssl/%.crt) \
@@ -91,6 +91,13 @@ ssl/client.crt: ssl/client.key ssl/client_ca.crt
 	openssl x509 -in ssl/temp.crt -out ssl/client.crt # to keep just the PEM cert
 	rm ssl/client.csr ssl/temp.crt
 
+# Client certificate with multi-parth DN, signed by the client CA:
+ssl/client-dn.crt: ssl/client-dn.key ssl/client_ca.crt
+	openssl req -new -key ssl/client-dn.key -out ssl/client-dn.csr -config client-dn.config
+	openssl ca -name client_ca -batch -out ssl/temp.crt -config cas.config -infiles ssl/client-dn.csr
+	openssl x509 -in ssl/temp.crt -out ssl/client-dn.crt # to keep just the PEM cert
+	rm ssl/client-dn.csr ssl/temp.crt
+
 # Another client certificate, signed by the client CA. This one is revoked.
 ssl/client-revoked.crt: ssl/client-revoked.key ssl/client_ca.crt client.config
 	openssl req -new -key ssl/client-revoked.key -out ssl/client-revoked.csr -config client.config
diff --git a/src/test/ssl/client-dn.config b/src/test/ssl/client-dn.config
new file mode 100644
index 0000000000..ee2df54bd6
--- /dev/null
+++ b/src/test/ssl/client-dn.config
@@ -0,0 +1,16 @@
+# An OpenSSL format CSR config file for creating a client certificate.
+#
+# The certificate is for user "ssltestuser-dn" with a multi-part DN
+
+[ req ]
+distinguished_name     = req_distinguished_name
+prompt                 = no
+
+[ req_distinguished_name ]
+O 					   = PGDG
+0.OU					   = Engineering
+1.OU					   = Testing
+CN                     = ssltestuser-dn
+
+# no extensions in client certs
+[ v3_req ]
diff --git a/src/test/ssl/ssl/client-dn.crt b/src/test/ssl/ssl/client-dn.crt
new file mode 100644
index 0000000000..f2f6a62dad
--- /dev/null
+++ b/src/test/ssl/ssl/client-dn.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDBjCCAe4CAQEwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UEAww3VGVzdCBDQSBm
+b3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IGNsaWVudCBjZXJ0czAe
+Fw0yMTAzMDUyMDUyNDVaFw00ODA3MjEyMDUyNDVaMFAxDTALBgNVBAoMBFBHREcx
+FDASBgNVBAsMC0VuZ2luZWVyaW5nMRAwDgYDVQQLDAdUZXN0aW5nMRcwFQYDVQQD
+DA5zc2x0ZXN0dXNlci1kbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AMRLriq2Sh8+N4bhVtRUp/MAEsLQK6u/GotMSmiSr9K31YBYOvNzw8liKt4Rmnh5
+zmsdXJBW8erPNpkUAy9tFRCAx0YobhWCSfyX3orEdrhDrLFihA62zXQC69T0u4Yp
+PSXGd0yCAcOZERQ4CQVgqnsh7Kmx5QaQnqxaz4OVPArWFJP4RQBT/l+r+kCeAn6h
+qvbSbxY3FoCElQq0EF5x1F2pjL+HcBvjeI+GP430gVeJJX0RaG14Fp4v9MQT6zv/
+gvvjHC8l7YSJUROjeUzLZpUnj/ik4yrtT4av/TDGTSOpGs5qEATqk4hxAUEWw6TJ
+RoLh3Oq2N5KuzDmKBBskLX0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAL2H54oyx
+pNkcgFF79lwc4c/Jda7j0wrZQIw5CWwO0MdCozJGRIEAA5WXA8b5THo1ZkaWv+sh
+lWnCOflBtGnEpD7dUpMW9lxGL5clMeMf3CoNYBb7zBofm+oTJytCzXHNftB4hCZj
+pvN79bNT4msWbmxDyi75nfbEfzK1BKnfCg+DWBBjEnHC8VzgDq6ACN6FEoyFb+fr
+dlDoof+S7k8jYAzhxwySI5DnMzr9OIwnepWfx9HENsasAighc8vFSEouShvsOlYS
+L0OIb9Tn6M5q1tWoLHulQsQYDPzaO/1M7ubsr5xCx1ReDK4gaNwS3YXn/2KE9Kco
+aKCrL89AjQrJPA==
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/client-dn.key b/src/test/ssl/ssl/client-dn.key
new file mode 100644
index 0000000000..1d67ef0408
--- /dev/null
+++ b/src/test/ssl/ssl/client-dn.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAxEuuKrZKHz43huFW1FSn8wASwtArq78ai0xKaJKv0rfVgFg6
+83PDyWIq3hGaeHnOax1ckFbx6s82mRQDL20VEIDHRihuFYJJ/JfeisR2uEOssWKE
+DrbNdALr1PS7hik9JcZ3TIIBw5kRFDgJBWCqeyHsqbHlBpCerFrPg5U8CtYUk/hF
+AFP+X6v6QJ4CfqGq9tJvFjcWgISVCrQQXnHUXamMv4dwG+N4j4Y/jfSBV4klfRFo
+bXgWni/0xBPrO/+C++McLyXthIlRE6N5TMtmlSeP+KTjKu1Phq/9MMZNI6kazmoQ
+BOqTiHEBQRbDpMlGguHc6rY3kq7MOYoEGyQtfQIDAQABAoIBABqL3Zb7JhUJlfrQ
+uKxocnojdWYRPwawBof2Hk38IHkP0XjU9cv8yOqQMxnrKYfHeUn1I5KFn5vQwCJ9
+mVytlN6xe8GaMCEKiLT3WOpNXXzX8h/fIdrXj/tzda9MFZw0MYfNSk73egOYzL1+
+QoIOq5+RW+8rFr0Hi93lPhEeeotAYWDQgx9Ye/NSW6vK2m47hdBKf9SBsWs+Vafa
+mC9Bf4LQqRYSJZee1zDwIh+Om7/JTsjMZYU0/lpycRz7V5uHbamXKlOXF54ow3Wn
+CJ9eVVWo7sb3CaeJ0p2sHIFp89ybMQ2vvmNr6aJNtZWd5WYxsjKs40rVq6DiUlFn
+T6CK7uECgYEA/Ks4/OnZnorhaHwYTs0LqiPSM7oZw4qchCNDMoE3WngsaZoWUKmr
+2JTY6uYP/B+oWgwPBdDiPRDeGqtVNZSAVsZEDMbiqZxwHaLi9OKJ7sKgK8Q6ANV1
+q5qgH1yXXygWhlol/Nf9bbnGWWoN+33zvnADeKRcT/1gZLEQpJ46DHUCgYEAxuIx
+k/EOOT9kyC5WrBDY3l7veb/WGRQgXTXiCJaO4d7IYh8UpUXlg0ZYF4RfeKRsSd07
+n9QdW6ImrtDloNyG6HnDknYsPRUs8JcuuyrxaOsZ/p9LS76ItNV7gzREf4N/7jrD
+c6TJappgXm+dgXg6ENuyk05hzjT6qdvm9V80m+kCgYEA7kfXRYSP61lT/AJTtjTf
+FEQV3xxZYbRdqKvMmluLxTDhyXE8LDPm0SiGbPgsCPwd+1W18SktwqMeoo4DnLUA
+V1VBJb+GUKgsf3Z2jLT7mYRIIx46CUFFaGE5MnpScrXOkEOB4bIb2RfCu94tc4gz
+jtv6GhL+z5zHBA6MAIMLgWUCgYAlynNLPkHKpP4cf5mehnD/CCEPDGG9UDK6I3P4
+18r8pl2DL463vOlYoXQ5u8B8ZxngizY6L48Ii244R59qipzj7cc4vFW5oZ1xdfi+
+PfGzUwEUfeZL1T+axPn8O2FMrYsQlH/xKH3RUNZA+4p9QIAgFe7/yKQTD8QVpKBl
+PZr8iQKBgBjdrgMt1Az98ECXJCjM4uui2S9UenNQVmhmxgZUpHqfNk+WEvIIthDi
+FEJPSTHyhTI9XIrhhwNkW3UZMjMndAiNylXGfJdr/xGwLM57t5HhGgljSboV7Mnw
+RFnh2FZxa3i/8g+4lAPZNwU0W/JU46wgg4C2Eu/Ne7jA8XUXYu9t
+-----END RSA PRIVATE KEY-----
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 864f6e209f..baec64ed7f 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 100;
+	plan tests => 103;
 }
 
 #### Some configuration
@@ -40,7 +40,7 @@ my $common_connstr;
 my @keys = (
 	"client",     "client-revoked",
 	"client-der", "client-encrypted-pem",
-	"client-encrypted-der");
+	"client-encrypted-der", "client-dn");
 foreach my $key (@keys)
 {
 	copy("ssl/${key}.key", "ssl/${key}_tmp.key")
@@ -453,6 +453,37 @@ test_connect_fails(
 	"certificate authorization fails with correct client cert and wrong password in encrypted PEM format"
 );
 
+
+# correct client cert using whole DN
+my $dn_connstr = $common_connstr;
+$dn_connstr =~ s/certdb/certdb_dn/;
+
+test_connect_ok(
+	$dn_connstr,
+	"user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
+	"certificate authorization succeeds with DN mapping"
+);
+
+# same thing but with a regex
+$dn_connstr =~ s/certdb_dn/certdb_dn_re/;
+
+test_connect_ok(
+	$dn_connstr,
+	"user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
+	"certificate authorization succeeds with DN regex mapping"
+);
+
+# same thing but using explicit CN
+$dn_connstr =~ s/certdb_dn_re/certdb_cn/;
+
+test_connect_ok(
+	$dn_connstr,
+	"user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
+	"certificate authorization succeeds with CN mapping"
+);
+
+
+
 TODO:
 {
 	# these tests are left here waiting on us to get better pty support
diff --git a/src/test/ssl/t/SSLServer.pm b/src/test/ssl/t/SSLServer.pm
index 5ec5e0dac8..c494c1ad1c 100644
--- a/src/test/ssl/t/SSLServer.pm
+++ b/src/test/ssl/t/SSLServer.pm
@@ -109,6 +109,9 @@ sub configure_test_server_for_ssl
 	$node->psql('postgres', "CREATE USER yetanotheruser");
 	$node->psql('postgres', "CREATE DATABASE trustdb");
 	$node->psql('postgres', "CREATE DATABASE certdb");
+	$node->psql('postgres', "CREATE DATABASE certdb_dn");
+	$node->psql('postgres', "CREATE DATABASE certdb_dn_re");
+	$node->psql('postgres', "CREATE DATABASE certdb_cn");
 	$node->psql('postgres', "CREATE DATABASE verifydb");
 
 	# Update password of each user as needed.
@@ -217,7 +220,20 @@ sub configure_hba_for_ssl
 	  "hostssl verifydb        yetanotheruser  $servercidr            $authmethod        clientcert=verify-ca\n";
 	print $hba
 	  "hostssl certdb          all             $servercidr            cert\n";
+	print $hba
+	  "hostssl certdb_dn       all             $servercidr            cert clientname=DN map=dn\n",
+	  "hostssl certdb_dn_re    all             $servercidr            cert clientname=DN map=dnre\n",
+	  "hostssl certdb_cn       all             $servercidr            cert clientname=CN map=cn\n";
 	close $hba;
+
+	# Also set the ident maps. Note: fields with commas must be quoted
+	open my $map, ">", "$pgdata/pg_ident.conf";
+	print $map
+	  "# MAPNAME       SYSTEM-USERNAME                           PG-USERNAME\n",
+	  "dn             \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\"    ssltestuser\n",
+	  "dnre           \"/^.*OU=Testing,.*\$\"                    ssltestuser\n",
+	  "cn              ssltestuser-dn                            ssltestuser\n";
+
 	return;
 }
 

Reply via email to