Currently we only match the Common Name (CN) of a client certificate when authenticating a user. The attached patch allows matching the entire Distinguished Name (DN) of the certificate. This is enabled by the HBA line option "clientname", which can take the values "CN" or "DN". "CN" is the default.
The idea is that you might have a role with a CN of, say, "dbauser" in two different parts of the organization, say one with "OU=marketing" and the other with "OU=engineering", and you only want to allow access to one of them. This feature is best used in conjunction with a map. e.g. in testing I have this pg_hba.conf line: hostssl all all 127.0.0.1/32 cert clientname=DN map=dn and this pg_ident.conf line: dn /^C=US,ST=North.Carolina,O=test,OU=eng,CN=andrew$ andrew If people like this idea I'll add tests and docco and add it to the next CF. cheers andrew -- Andrew Dunstan EDB: https://www.enterprisedb.com "
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index d132c5cb48..7e40de82e0 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -2869,12 +2869,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", @@ -2882,8 +2885,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) { /* @@ -2894,7 +2897,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 e10260051f..dbb32c86bb 100644 --- a/src/backend/libpq/be-secure-openssl.c +++ b/src/backend/libpq/be-secure-openssl.c @@ -520,22 +520,30 @@ 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); - if (len != -1) + if (!x509name) { - char *peer_cn; + return -1; + } + len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0); + if (len != -1) + { 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) { @@ -559,6 +567,32 @@ aloop: port->peer_cn = peer_cn; } + + bio = BIO_new(BIO_s_mem()); + if (!bio) + { + return -1; + } + /* use commas instead of slashes */ + X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_SEP_COMMA_PLUS); + 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); + return -1; + } + + BIO_free(bio); + + port->peer_dn = peer_dn; + port->peer_cert_valid = true; } @@ -590,6 +624,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 2ae507a902..ef1320e686 100644 --- a/src/backend/libpq/be-secure.c +++ b/src/backend/libpq/be-secure.c @@ -120,7 +120,7 @@ secure_open_server(Port *port) ereport(DEBUG2, (errmsg("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 3a78d2043e..5e17382085 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -1765,6 +1765,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 0a23281ad5..bc56551d2e 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -189,6 +189,7 @@ typedef struct Port */ bool ssl_in_use; char *peer_cn; + char *peer_dn; bool peer_cert_valid; /*