summaryrefslogtreecommitdiff
path: root/src/backend/libpq
diff options
context:
space:
mode:
authorAndrew Dunstan <andrew@dunslane.net>2021-03-29 15:31:22 -0400
committerAndrew Dunstan <andrew@dunslane.net>2021-03-29 15:49:39 -0400
commit6d7a6feac48b1970c4cd127ee65d4c487acbb5e9 (patch)
tree8728162431269b3ae654eddb1d3a8e1c99972ec3 /src/backend/libpq
parentefcc7572f532ea564fedc6359c2df43045ee7908 (diff)
downloadpostgresql-6d7a6feac48b1970c4cd127ee65d4c487acbb5e9.tar.gz
Allow matching the DN of a client certificate for authentication
Currently we only recognize the Common Name (CN) of a certificate's subject to be matched against the user name. Thus certificates with subjects '/OU=eng/CN=fred' and '/OU=sales/CN=fred' will have the same connection rights. This patch provides an option to match the whole Distinguished Name (DN) instead of just the CN. On any hba line using client certificate identity, there is an option 'clientname' which can have values of 'DN' or 'CN'. The default is 'CN', the current procedure. The DN is matched against the RFC2253 formatted DN, which looks like 'CN=fred,OU=eng'. This facility of probably best used in conjunction with an ident map. Discussion: https://postgr.es/m/92e70110-9273-d93c-5913-0bccb6562740@dunslane.net Reviewed-By: Michael Paquier, Daniel Gustafsson, Jacob Champion
Diffstat (limited to 'src/backend/libpq')
-rw-r--r--src/backend/libpq/auth.c34
-rw-r--r--src/backend/libpq/be-secure-openssl.c61
-rw-r--r--src/backend/libpq/be-secure.c5
-rw-r--r--src/backend/libpq/hba.c31
4 files changed, 117 insertions, 14 deletions
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..9dc28e19aa 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2800,12 +2800,23 @@ static int
CheckCertAuth(Port *port)
{
int status_check_usermap = STATUS_ERROR;
+ char *peer_username = NULL;
Assert(port->ssl);
+ /* select the correct field to compare */
+ switch (port->hba->clientcertname)
+ {
+ case clientCertDN:
+ peer_username = port->peer_dn;
+ break;
+ case clientCertCN:
+ peer_username = port->peer_cn;
+ }
+
/* Make sure we have received a username in the certificate */
- if (port->peer_cn == NULL ||
- strlen(port->peer_cn) <= 0)
+ 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 +2824,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)
{
/*
@@ -2824,9 +2835,18 @@ 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",
- port->user_name)));
+ switch (port->hba->clientcertname)
+ {
+ case clientCertDN:
+ ereport(LOG,
+ (errmsg("certificate validation (clientcert=verify-full) failed for user \"%s\": DN mismatch",
+ port->user_name)));
+ break;
+ case clientCertCN:
+ ereport(LOG,
+ (errmsg("certificate validation (clientcert=verify-full) failed for user \"%s\": CN mismatch",
+ port->user_name)));
+ }
}
}
return status_check_usermap;
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 5ce3f27855..40deab13c7 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -551,22 +551,26 @@ 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 and 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_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)
{
@@ -590,6 +594,47 @@ aloop:
port->peer_cn = peer_cn;
}
+
+ bio = BIO_new(BIO_s_mem());
+ if (!bio)
+ {
+ pfree(port->peer_cn);
+ port->peer_cn = NULL;
+ return -1;
+ }
+ /*
+ * RFC2253 is the closest thing to an accepted standard format for
+ * DNs. We have documented how to produce this format from a
+ * certificate. It uses commas instead of slashes for delimiters,
+ * which make regular expression matching a bit easier. Also note that
+ * it prints the Subject fields in reverse order.
+ */
+ X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+ if (BIO_get_mem_ptr(bio, &bio_buf) <= 0)
+ {
+ BIO_free(bio);
+ pfree(port->peer_cn);
+ port->peer_cn = NULL;
+ return -1;
+ }
+ peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+ memcpy(peer_dn, bio_buf->data, bio_buf->length);
+ len = bio_buf->length;
+ BIO_free(bio);
+ peer_dn[len] = '\0';
+ if (len != strlen(peer_dn))
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("SSL certificate's distinguished name contains embedded null")));
+ pfree(peer_dn);
+ pfree(port->peer_cn);
+ port->peer_cn = NULL;
+ return -1;
+ }
+
+ port->peer_dn = peer_dn;
+
port->peer_cert_valid = true;
}
@@ -618,6 +663,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..8ef083200a 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -120,8 +120,9 @@ secure_open_server(Port *port)
r = be_tls_open_server(port);
ereport(DEBUG2,
- (errmsg_internal("SSL connection from \"%s\"",
- port->peer_cn ? port->peer_cn : "(anonymous)")));
+ (errmsg_internal("SSL connection from DN:\"%s\" CN:\"%s\"",
+ port->peer_dn ? port->peer_dn : "(anonymous)",
+ port->peer_cn ? port->peer_cn : "(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");