Skip to content

Commit

Permalink
Allow matching the DN of a client certificate for authentication
Browse files Browse the repository at this point in the history
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
  • Loading branch information
adunstan committed Mar 29, 2021
1 parent efcc757 commit 6d7a6fe
Show file tree
Hide file tree
Showing 13 changed files with 266 additions and 18 deletions.
24 changes: 23 additions & 1 deletion doc/src/sgml/client-auth.sgml
Expand Up @@ -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
Expand All @@ -612,6 +612,28 @@ 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
<ulink url="https://tools.ietf.org/html/rfc2253">RFC 2253</ulink>
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 needs to be taken when using this option, especially when using
regular expression matching against the <literal>DN</literal>.
</para>
</listitem>
</varlistentry>
</variablelist>
Expand Down
34 changes: 27 additions & 7 deletions src/backend/libpq/auth.c
Expand Up @@ -2800,21 +2800,32 @@ 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",
port->user_name)));
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)
{
/*
Expand All @@ -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;
Expand Down
61 changes: 56 additions & 5 deletions src/backend/libpq/be-secure-openssl.c
Expand Up @@ -551,22 +551,26 @@ be_tls_open_server(Port *port)
/* 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)
{
Expand All @@ -590,6 +594,47 @@ be_tls_open_server(Port *port)

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;
}

Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/backend/libpq/be-secure.c
Expand Up @@ -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;
Expand Down
31 changes: 31 additions & 0 deletions src/backend/libpq/hba.c
Expand Up @@ -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");
Expand Down
7 changes: 7 additions & 0 deletions src/include/libpq/hba.h
Expand Up @@ -71,6 +71,12 @@ typedef enum ClientCertMode
clientCertFull
} ClientCertMode;

typedef enum ClientCertName
{
clientCertCN,
clientCertDN
} ClientCertName;

typedef struct HbaLine
{
int linenumber;
Expand Down Expand Up @@ -101,6 +107,7 @@ typedef struct HbaLine
char *ldapprefix;
char *ldapsuffix;
ClientCertMode clientcert;
ClientCertName clientcertname;
char *krb_realm;
bool include_realm;
bool compat_realm;
Expand Down
1 change: 1 addition & 0 deletions src/include/libpq/libpq-be.h
Expand Up @@ -195,6 +195,7 @@ typedef struct Port
*/
bool ssl_in_use;
char *peer_cn;
char *peer_dn;
bool peer_cert_valid;

/*
Expand Down
9 changes: 8 additions & 1 deletion src/test/ssl/Makefile
Expand Up @@ -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) \
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions 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 ]
19 changes: 19 additions & 0 deletions 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-----
27 changes: 27 additions & 0 deletions 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-----

0 comments on commit 6d7a6fe

Please sign in to comment.