Skip to content

Commit 6d7a6fe

Browse files
committed
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
1 parent efcc757 commit 6d7a6fe

File tree

13 files changed

+266
-18
lines changed

13 files changed

+266
-18
lines changed

doc/src/sgml/client-auth.sgml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
598598
</para>
599599

600600
<para>
601-
In addition to the method-specific options listed below, there is one
601+
In addition to the method-specific options listed below, there is a
602602
method-independent authentication option <literal>clientcert</literal>, which
603603
can be specified in any <literal>hostssl</literal> record.
604604
This option can be set to <literal>verify-ca</literal> or
@@ -612,6 +612,28 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
612612
the verification of client certificates with any authentication
613613
method that supports <literal>hostssl</literal> entries.
614614
</para>
615+
<para>
616+
On any record using client certificate authentication (i.e. one
617+
using the <literal>cert</literal> authentication method or one
618+
using the <literal>clientcert</literal> option), you can specify
619+
which part of the client certificate credentials to match using
620+
the <literal>clientname</literal> option. This option can have one
621+
of two values. If you specify <literal>clientname=CN</literal>, which
622+
is the default, the username is matched against the certificate's
623+
<literal>Common Name (CN)</literal>. If instead you specify
624+
<literal>clientname=DN</literal> the username is matched against the
625+
entire <literal>Distinguished Name (DN)</literal> of the certificate.
626+
This option is probably best used in conjunction with a username map.
627+
The comparison is done with the <literal>DN</literal> in
628+
<ulink url="https://tools.ietf.org/html/rfc2253">RFC 2253</ulink>
629+
format. To see the <literal>DN</literal> of a client certificate
630+
in this format, do
631+
<programlisting>
632+
openssl x509 -in myclient.crt -noout --subject -nameopt RFC2253 | sed "s/^subject=//"
633+
</programlisting>
634+
Care needs to be taken when using this option, especially when using
635+
regular expression matching against the <literal>DN</literal>.
636+
</para>
615637
</listitem>
616638
</varlistentry>
617639
</variablelist>

src/backend/libpq/auth.c

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2800,21 +2800,32 @@ static int
28002800
CheckCertAuth(Port *port)
28012801
{
28022802
int status_check_usermap = STATUS_ERROR;
2803+
char *peer_username = NULL;
28032804

28042805
Assert(port->ssl);
28052806

2807+
/* select the correct field to compare */
2808+
switch (port->hba->clientcertname)
2809+
{
2810+
case clientCertDN:
2811+
peer_username = port->peer_dn;
2812+
break;
2813+
case clientCertCN:
2814+
peer_username = port->peer_cn;
2815+
}
2816+
28062817
/* Make sure we have received a username in the certificate */
2807-
if (port->peer_cn == NULL ||
2808-
strlen(port->peer_cn) <= 0)
2818+
if (peer_username == NULL ||
2819+
strlen(peer_username) <= 0)
28092820
{
28102821
ereport(LOG,
28112822
(errmsg("certificate authentication failed for user \"%s\": client certificate contains no user name",
28122823
port->user_name)));
28132824
return STATUS_ERROR;
28142825
}
28152826

2816-
/* Just pass the certificate cn to the usermap check */
2817-
status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
2827+
/* Just pass the certificate cn/dn to the usermap check */
2828+
status_check_usermap = check_usermap(port->hba->usermap, port->user_name, peer_username, false);
28182829
if (status_check_usermap != STATUS_OK)
28192830
{
28202831
/*
@@ -2824,9 +2835,18 @@ CheckCertAuth(Port *port)
28242835
*/
28252836
if (port->hba->clientcert == clientCertFull && port->hba->auth_method != uaCert)
28262837
{
2827-
ereport(LOG,
2828-
(errmsg("certificate validation (clientcert=verify-full) failed for user \"%s\": CN mismatch",
2829-
port->user_name)));
2838+
switch (port->hba->clientcertname)
2839+
{
2840+
case clientCertDN:
2841+
ereport(LOG,
2842+
(errmsg("certificate validation (clientcert=verify-full) failed for user \"%s\": DN mismatch",
2843+
port->user_name)));
2844+
break;
2845+
case clientCertCN:
2846+
ereport(LOG,
2847+
(errmsg("certificate validation (clientcert=verify-full) failed for user \"%s\": CN mismatch",
2848+
port->user_name)));
2849+
}
28302850
}
28312851
}
28322852
return status_check_usermap;

src/backend/libpq/be-secure-openssl.c

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -551,22 +551,26 @@ be_tls_open_server(Port *port)
551551
/* Get client certificate, if available. */
552552
port->peer = SSL_get_peer_certificate(port->ssl);
553553

554-
/* and extract the Common Name from it. */
554+
/* and extract the Common Name and Distinguished Name from it. */
555555
port->peer_cn = NULL;
556+
port->peer_dn = NULL;
556557
port->peer_cert_valid = false;
557558
if (port->peer != NULL)
558559
{
559560
int len;
561+
X509_NAME *x509name = X509_get_subject_name(port->peer);
562+
char *peer_dn;
563+
BIO *bio = NULL;
564+
BUF_MEM *bio_buf = NULL;
560565

561-
len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
562-
NID_commonName, NULL, 0);
566+
len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
563567
if (len != -1)
564568
{
565569
char *peer_cn;
566570

567571
peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
568-
r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
569-
NID_commonName, peer_cn, len + 1);
572+
r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
573+
len + 1);
570574
peer_cn[len] = '\0';
571575
if (r != len)
572576
{
@@ -590,6 +594,47 @@ be_tls_open_server(Port *port)
590594

591595
port->peer_cn = peer_cn;
592596
}
597+
598+
bio = BIO_new(BIO_s_mem());
599+
if (!bio)
600+
{
601+
pfree(port->peer_cn);
602+
port->peer_cn = NULL;
603+
return -1;
604+
}
605+
/*
606+
* RFC2253 is the closest thing to an accepted standard format for
607+
* DNs. We have documented how to produce this format from a
608+
* certificate. It uses commas instead of slashes for delimiters,
609+
* which make regular expression matching a bit easier. Also note that
610+
* it prints the Subject fields in reverse order.
611+
*/
612+
X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
613+
if (BIO_get_mem_ptr(bio, &bio_buf) <= 0)
614+
{
615+
BIO_free(bio);
616+
pfree(port->peer_cn);
617+
port->peer_cn = NULL;
618+
return -1;
619+
}
620+
peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
621+
memcpy(peer_dn, bio_buf->data, bio_buf->length);
622+
len = bio_buf->length;
623+
BIO_free(bio);
624+
peer_dn[len] = '\0';
625+
if (len != strlen(peer_dn))
626+
{
627+
ereport(COMMERROR,
628+
(errcode(ERRCODE_PROTOCOL_VIOLATION),
629+
errmsg("SSL certificate's distinguished name contains embedded null")));
630+
pfree(peer_dn);
631+
pfree(port->peer_cn);
632+
port->peer_cn = NULL;
633+
return -1;
634+
}
635+
636+
port->peer_dn = peer_dn;
637+
593638
port->peer_cert_valid = true;
594639
}
595640

@@ -618,6 +663,12 @@ be_tls_close(Port *port)
618663
pfree(port->peer_cn);
619664
port->peer_cn = NULL;
620665
}
666+
667+
if (port->peer_dn)
668+
{
669+
pfree(port->peer_dn);
670+
port->peer_dn = NULL;
671+
}
621672
}
622673

623674
ssize_t

src/backend/libpq/be-secure.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,9 @@ secure_open_server(Port *port)
120120
r = be_tls_open_server(port);
121121

122122
ereport(DEBUG2,
123-
(errmsg_internal("SSL connection from \"%s\"",
124-
port->peer_cn ? port->peer_cn : "(anonymous)")));
123+
(errmsg_internal("SSL connection from DN:\"%s\" CN:\"%s\"",
124+
port->peer_dn ? port->peer_dn : "(anonymous)",
125+
port->peer_cn ? port->peer_cn : "(anonymous)")));
125126
#endif
126127

127128
return r;

src/backend/libpq/hba.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,6 +1753,37 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
17531753
return false;
17541754
}
17551755
}
1756+
else if (strcmp(name, "clientname") == 0)
1757+
{
1758+
if (hbaline->conntype != ctHostSSL)
1759+
{
1760+
ereport(elevel,
1761+
(errcode(ERRCODE_CONFIG_FILE_ERROR),
1762+
errmsg("clientname can only be configured for \"hostssl\" rows"),
1763+
errcontext("line %d of configuration file \"%s\"",
1764+
line_num, HbaFileName)));
1765+
*err_msg = "clientname can only be configured for \"hostssl\" rows";
1766+
return false;
1767+
}
1768+
1769+
if (strcmp(val, "CN") == 0)
1770+
{
1771+
hbaline->clientcertname = clientCertCN;
1772+
}
1773+
else if (strcmp(val, "DN") == 0)
1774+
{
1775+
hbaline->clientcertname = clientCertDN;
1776+
}
1777+
else
1778+
{
1779+
ereport(elevel,
1780+
(errcode(ERRCODE_CONFIG_FILE_ERROR),
1781+
errmsg("invalid value for clientname: \"%s\"", val),
1782+
errcontext("line %d of configuration file \"%s\"",
1783+
line_num, HbaFileName)));
1784+
return false;
1785+
}
1786+
}
17561787
else if (strcmp(name, "pamservice") == 0)
17571788
{
17581789
REQUIRE_AUTH_OPTION(uaPAM, "pamservice", "pam");

src/include/libpq/hba.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ typedef enum ClientCertMode
7171
clientCertFull
7272
} ClientCertMode;
7373

74+
typedef enum ClientCertName
75+
{
76+
clientCertCN,
77+
clientCertDN
78+
} ClientCertName;
79+
7480
typedef struct HbaLine
7581
{
7682
int linenumber;
@@ -101,6 +107,7 @@ typedef struct HbaLine
101107
char *ldapprefix;
102108
char *ldapsuffix;
103109
ClientCertMode clientcert;
110+
ClientCertName clientcertname;
104111
char *krb_realm;
105112
bool include_realm;
106113
bool compat_realm;

src/include/libpq/libpq-be.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ typedef struct Port
195195
*/
196196
bool ssl_in_use;
197197
char *peer_cn;
198+
char *peer_dn;
198199
bool peer_cert_valid;
199200

200201
/*

src/test/ssl/Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export with_ssl
1818
CERTIFICATES := server_ca server-cn-and-alt-names \
1919
server-cn-only server-single-alt-name server-multiple-alt-names \
2020
server-no-names server-revoked server-ss \
21-
client_ca client client-revoked \
21+
client_ca client client-dn client-revoked \
2222
root_ca
2323

2424
SSLFILES := $(CERTIFICATES:%=ssl/%.key) $(CERTIFICATES:%=ssl/%.crt) \
@@ -91,6 +91,13 @@ ssl/client.crt: ssl/client.key ssl/client_ca.crt
9191
openssl x509 -in ssl/temp.crt -out ssl/client.crt # to keep just the PEM cert
9292
rm ssl/client.csr ssl/temp.crt
9393

94+
# Client certificate with multi-parth DN, signed by the client CA:
95+
ssl/client-dn.crt: ssl/client-dn.key ssl/client_ca.crt
96+
openssl req -new -key ssl/client-dn.key -out ssl/client-dn.csr -config client-dn.config
97+
openssl ca -name client_ca -batch -out ssl/temp.crt -config cas.config -infiles ssl/client-dn.csr
98+
openssl x509 -in ssl/temp.crt -out ssl/client-dn.crt # to keep just the PEM cert
99+
rm ssl/client-dn.csr ssl/temp.crt
100+
94101
# Another client certificate, signed by the client CA. This one is revoked.
95102
ssl/client-revoked.crt: ssl/client-revoked.key ssl/client_ca.crt client.config
96103
openssl req -new -key ssl/client-revoked.key -out ssl/client-revoked.csr -config client.config

src/test/ssl/client-dn.config

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# An OpenSSL format CSR config file for creating a client certificate.
2+
#
3+
# The certificate is for user "ssltestuser-dn" with a multi-part DN
4+
5+
[ req ]
6+
distinguished_name = req_distinguished_name
7+
prompt = no
8+
9+
[ req_distinguished_name ]
10+
O = PGDG
11+
0.OU = Engineering
12+
1.OU = Testing
13+
CN = ssltestuser-dn
14+
15+
# no extensions in client certs
16+
[ v3_req ]

src/test/ssl/ssl/client-dn.crt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDBjCCAe4CAQEwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UEAww3VGVzdCBDQSBm
3+
b3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IGNsaWVudCBjZXJ0czAe
4+
Fw0yMTAzMDUyMDUyNDVaFw00ODA3MjEyMDUyNDVaMFAxDTALBgNVBAoMBFBHREcx
5+
FDASBgNVBAsMC0VuZ2luZWVyaW5nMRAwDgYDVQQLDAdUZXN0aW5nMRcwFQYDVQQD
6+
DA5zc2x0ZXN0dXNlci1kbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
7+
AMRLriq2Sh8+N4bhVtRUp/MAEsLQK6u/GotMSmiSr9K31YBYOvNzw8liKt4Rmnh5
8+
zmsdXJBW8erPNpkUAy9tFRCAx0YobhWCSfyX3orEdrhDrLFihA62zXQC69T0u4Yp
9+
PSXGd0yCAcOZERQ4CQVgqnsh7Kmx5QaQnqxaz4OVPArWFJP4RQBT/l+r+kCeAn6h
10+
qvbSbxY3FoCElQq0EF5x1F2pjL+HcBvjeI+GP430gVeJJX0RaG14Fp4v9MQT6zv/
11+
gvvjHC8l7YSJUROjeUzLZpUnj/ik4yrtT4av/TDGTSOpGs5qEATqk4hxAUEWw6TJ
12+
RoLh3Oq2N5KuzDmKBBskLX0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAL2H54oyx
13+
pNkcgFF79lwc4c/Jda7j0wrZQIw5CWwO0MdCozJGRIEAA5WXA8b5THo1ZkaWv+sh
14+
lWnCOflBtGnEpD7dUpMW9lxGL5clMeMf3CoNYBb7zBofm+oTJytCzXHNftB4hCZj
15+
pvN79bNT4msWbmxDyi75nfbEfzK1BKnfCg+DWBBjEnHC8VzgDq6ACN6FEoyFb+fr
16+
dlDoof+S7k8jYAzhxwySI5DnMzr9OIwnepWfx9HENsasAighc8vFSEouShvsOlYS
17+
L0OIb9Tn6M5q1tWoLHulQsQYDPzaO/1M7ubsr5xCx1ReDK4gaNwS3YXn/2KE9Kco
18+
aKCrL89AjQrJPA==
19+
-----END CERTIFICATE-----

src/test/ssl/ssl/client-dn.key

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEowIBAAKCAQEAxEuuKrZKHz43huFW1FSn8wASwtArq78ai0xKaJKv0rfVgFg6
3+
83PDyWIq3hGaeHnOax1ckFbx6s82mRQDL20VEIDHRihuFYJJ/JfeisR2uEOssWKE
4+
DrbNdALr1PS7hik9JcZ3TIIBw5kRFDgJBWCqeyHsqbHlBpCerFrPg5U8CtYUk/hF
5+
AFP+X6v6QJ4CfqGq9tJvFjcWgISVCrQQXnHUXamMv4dwG+N4j4Y/jfSBV4klfRFo
6+
bXgWni/0xBPrO/+C++McLyXthIlRE6N5TMtmlSeP+KTjKu1Phq/9MMZNI6kazmoQ
7+
BOqTiHEBQRbDpMlGguHc6rY3kq7MOYoEGyQtfQIDAQABAoIBABqL3Zb7JhUJlfrQ
8+
uKxocnojdWYRPwawBof2Hk38IHkP0XjU9cv8yOqQMxnrKYfHeUn1I5KFn5vQwCJ9
9+
mVytlN6xe8GaMCEKiLT3WOpNXXzX8h/fIdrXj/tzda9MFZw0MYfNSk73egOYzL1+
10+
QoIOq5+RW+8rFr0Hi93lPhEeeotAYWDQgx9Ye/NSW6vK2m47hdBKf9SBsWs+Vafa
11+
mC9Bf4LQqRYSJZee1zDwIh+Om7/JTsjMZYU0/lpycRz7V5uHbamXKlOXF54ow3Wn
12+
CJ9eVVWo7sb3CaeJ0p2sHIFp89ybMQ2vvmNr6aJNtZWd5WYxsjKs40rVq6DiUlFn
13+
T6CK7uECgYEA/Ks4/OnZnorhaHwYTs0LqiPSM7oZw4qchCNDMoE3WngsaZoWUKmr
14+
2JTY6uYP/B+oWgwPBdDiPRDeGqtVNZSAVsZEDMbiqZxwHaLi9OKJ7sKgK8Q6ANV1
15+
q5qgH1yXXygWhlol/Nf9bbnGWWoN+33zvnADeKRcT/1gZLEQpJ46DHUCgYEAxuIx
16+
k/EOOT9kyC5WrBDY3l7veb/WGRQgXTXiCJaO4d7IYh8UpUXlg0ZYF4RfeKRsSd07
17+
n9QdW6ImrtDloNyG6HnDknYsPRUs8JcuuyrxaOsZ/p9LS76ItNV7gzREf4N/7jrD
18+
c6TJappgXm+dgXg6ENuyk05hzjT6qdvm9V80m+kCgYEA7kfXRYSP61lT/AJTtjTf
19+
FEQV3xxZYbRdqKvMmluLxTDhyXE8LDPm0SiGbPgsCPwd+1W18SktwqMeoo4DnLUA
20+
V1VBJb+GUKgsf3Z2jLT7mYRIIx46CUFFaGE5MnpScrXOkEOB4bIb2RfCu94tc4gz
21+
jtv6GhL+z5zHBA6MAIMLgWUCgYAlynNLPkHKpP4cf5mehnD/CCEPDGG9UDK6I3P4
22+
18r8pl2DL463vOlYoXQ5u8B8ZxngizY6L48Ii244R59qipzj7cc4vFW5oZ1xdfi+
23+
PfGzUwEUfeZL1T+axPn8O2FMrYsQlH/xKH3RUNZA+4p9QIAgFe7/yKQTD8QVpKBl
24+
PZr8iQKBgBjdrgMt1Az98ECXJCjM4uui2S9UenNQVmhmxgZUpHqfNk+WEvIIthDi
25+
FEJPSTHyhTI9XIrhhwNkW3UZMjMndAiNylXGfJdr/xGwLM57t5HhGgljSboV7Mnw
26+
RFnh2FZxa3i/8g+4lAPZNwU0W/JU46wgg4C2Eu/Ne7jA8XUXYu9t
27+
-----END RSA PRIVATE KEY-----

0 commit comments

Comments
 (0)