Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 3a0e385

Browse files
committed
Log details for client certificate failures
Currently, debugging client certificate verification failures is mostly limited to looking at the TLS alert code on the client side. For simple deployments, sometimes it's enough to see "sslv3 alert certificate revoked" and know exactly what needs to be fixed, but if you add any more complexity (multiple CA layers, misconfigured CA certificates, etc.), trying to debug what happened based on the TLS alert alone can be an exercise in frustration. Luckily, the server has more information about exactly what failed in the chain, and we already have the requisite callback implemented as a stub. We fill that in, collect the data, and pass the constructed error message back to the main code via a static variable. This lets us add our error details directly to the final "could not accept SSL connection" log message, as opposed to issuing intermediate LOGs. It ends up looking like LOG: connection received: host=localhost port=43112 LOG: could not accept SSL connection: certificate verify failed DETAIL: Client certificate verification failed at depth 1: unable to get local issuer certificate. Failed certificate data (unverified): subject "/CN=Test CA for PostgreSQL SSL regression test client certs", serial number 2315134995201656577, issuer "/CN=Test root CA for PostgreSQL SSL regression test suite". The length of the Subject and Issuer strings is limited to prevent malicious client certs from spamming the logs. In case the truncation makes things ambiguous, the certificate's serial number is also logged. Author: Jacob Champion <pchampion@vmware.com> Discussion: https://www.postgresql.org/message-id/flat/d13c4a5787c2a3f83705124f0391e0738c796751.camel@vmware.com
1 parent 507ba16 commit 3a0e385

File tree

7 files changed

+210
-7
lines changed

7 files changed

+210
-7
lines changed

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

+109-3
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ static bool ssl_is_server_start;
8181
static int ssl_protocol_version_to_openssl(int v);
8282
static const char *ssl_protocol_version_to_string(int v);
8383

84+
/* for passing data back from verify_cb() */
85+
static const char *cert_errdetail;
86+
8487
/* ------------------------------------------------------------ */
8588
/* Public interface */
8689
/* ------------------------------------------------------------ */
@@ -541,6 +544,7 @@ be_tls_open_server(Port *port)
541544
(errcode(ERRCODE_PROTOCOL_VIOLATION),
542545
errmsg("could not accept SSL connection: %s",
543546
SSLerrmessage(ecode)),
547+
cert_errdetail ? errdetail_internal("%s", cert_errdetail) : 0,
544548
give_proto_hint ?
545549
errhint("This may indicate that the client does not support any SSL protocol version between %s and %s.",
546550
ssl_min_protocol_version ?
@@ -549,6 +553,7 @@ be_tls_open_server(Port *port)
549553
ssl_max_protocol_version ?
550554
ssl_protocol_version_to_string(ssl_max_protocol_version) :
551555
MAX_OPENSSL_TLS_VERSION) : 0));
556+
cert_errdetail = NULL;
552557
break;
553558
case SSL_ERROR_ZERO_RETURN:
554559
ereport(COMMERROR,
@@ -1076,12 +1081,46 @@ dummy_ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata)
10761081
return 0;
10771082
}
10781083

1084+
/*
1085+
* Examines the provided certificate name, and if it's too long to log, modifies
1086+
* and truncates it. The return value is NULL if no truncation was needed; it
1087+
* otherwise points into the middle of the input string, and should not be
1088+
* freed.
1089+
*/
1090+
static char *
1091+
truncate_cert_name(char *name)
1092+
{
1093+
size_t namelen = strlen(name);
1094+
char *truncated;
1095+
1096+
/*
1097+
* Common Names are 64 chars max, so for a common case where the CN is the
1098+
* last field, we can still print the longest possible CN with a
1099+
* 7-character prefix (".../CN=[64 chars]"), for a reasonable limit of 71
1100+
* characters.
1101+
*/
1102+
#define MAXLEN 71
1103+
1104+
if (namelen <= MAXLEN)
1105+
return NULL;
1106+
1107+
/*
1108+
* Keep the end of the name, not the beginning, since the most specific
1109+
* field is likely to give users the most information.
1110+
*/
1111+
truncated = name + namelen - MAXLEN;
1112+
truncated[0] = truncated[1] = truncated[2] = '.';
1113+
1114+
#undef MAXLEN
1115+
1116+
return truncated;
1117+
}
1118+
10791119
/*
10801120
* Certificate verification callback
10811121
*
1082-
* This callback allows us to log intermediate problems during
1083-
* verification, but for now we'll see if the final error message
1084-
* contains enough information.
1122+
* This callback allows us to examine intermediate problems during
1123+
* verification, for later logging.
10851124
*
10861125
* This callback also allows us to override the default acceptance
10871126
* criteria (e.g., accepting self-signed or expired certs), but
@@ -1090,6 +1129,73 @@ dummy_ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata)
10901129
static int
10911130
verify_cb(int ok, X509_STORE_CTX *ctx)
10921131
{
1132+
int depth;
1133+
int errcode;
1134+
const char *errstring;
1135+
StringInfoData str;
1136+
X509 *cert;
1137+
1138+
if (ok)
1139+
{
1140+
/* Nothing to do for the successful case. */
1141+
return ok;
1142+
}
1143+
1144+
/* Pull all the information we have on the verification failure. */
1145+
depth = X509_STORE_CTX_get_error_depth(ctx);
1146+
errcode = X509_STORE_CTX_get_error(ctx);
1147+
errstring = X509_verify_cert_error_string(errcode);
1148+
1149+
initStringInfo(&str);
1150+
appendStringInfo(&str,
1151+
_("Client certificate verification failed at depth %d: %s."),
1152+
depth, errstring);
1153+
1154+
cert = X509_STORE_CTX_get_current_cert(ctx);
1155+
if (cert)
1156+
{
1157+
char *subject,
1158+
*issuer;
1159+
char *sub_truncated,
1160+
*iss_truncated;
1161+
char *serialno;
1162+
ASN1_INTEGER *sn;
1163+
BIGNUM *b;
1164+
1165+
/*
1166+
* Get the Subject and Issuer for logging, but don't let maliciously
1167+
* huge certs flood the logs.
1168+
*/
1169+
subject = X509_NAME_to_cstring(X509_get_subject_name(cert));
1170+
sub_truncated = truncate_cert_name(subject);
1171+
1172+
issuer = X509_NAME_to_cstring(X509_get_issuer_name(cert));
1173+
iss_truncated = truncate_cert_name(issuer);
1174+
1175+
/*
1176+
* Pull the serial number, too, in case a Subject is still ambiguous.
1177+
* This mirrors be_tls_get_peer_serial().
1178+
*/
1179+
sn = X509_get_serialNumber(cert);
1180+
b = ASN1_INTEGER_to_BN(sn, NULL);
1181+
serialno = BN_bn2dec(b);
1182+
1183+
appendStringInfoChar(&str, '\n');
1184+
appendStringInfo(&str,
1185+
_("Failed certificate data (unverified): subject \"%s\", serial number %s, issuer \"%s\"."),
1186+
sub_truncated ? sub_truncated : subject,
1187+
serialno ? serialno : _("unknown"),
1188+
iss_truncated ? iss_truncated : issuer);
1189+
1190+
BN_free(b);
1191+
OPENSSL_free(serialno);
1192+
pfree(issuer);
1193+
pfree(subject);
1194+
}
1195+
1196+
/* Store our detail message to be logged later. */
1197+
cert_errdetail = str.data;
1198+
10931199
return ok;
10941200
}
10951201

src/test/ssl/conf/client-long.config

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# An OpenSSL format CSR config file for creating a client certificate with a
2+
# long Subject.
3+
4+
[ req ]
5+
distinguished_name = req_distinguished_name
6+
prompt = no
7+
8+
[ req_distinguished_name ]
9+
# Common Names are 64 characters max
10+
CN = ssl-123456789012345678901234567890123456789012345678901234567890
11+
OU = Some Organizational Unit
12+
O = PostgreSQL Global Development Group
13+
14+
# no extensions in client certs

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

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDWjCCAkICCCAiBRIUREYAMA0GCSqGSIb3DQEBCwUAMEIxQDA+BgNVBAMMN1Rl
3+
c3QgQ0EgZm9yIFBvc3RncmVTUUwgU1NMIHJlZ3Jlc3Npb24gdGVzdCBjbGllbnQg
4+
Y2VydHMwHhcNMjIwNTEyMjE0NDQ3WhcNNDkwOTI3MjE0NDQ3WjCBnDEsMCoGA1UE
5+
CgwjUG9zdGdyZVNRTCBHbG9iYWwgRGV2ZWxvcG1lbnQgR3JvdXAxITAfBgNVBAsM
6+
GFNvbWUgT3JnYW5pemF0aW9uYWwgVW5pdDFJMEcGA1UEAwxAc3NsLTEyMzQ1Njc4
7+
OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2
8+
Nzg5MDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANgxmeHiVRuBTwlG
9+
Q1oM2M1ckQCI/o4hYcO9BYdxDYHiA7jy1WVenyj8BtUi5Aj9VDhpfiuewDarGQ5a
10+
TggD1pMjkw0MorBKBr9+1u1xGH/8Q3lkgU+OQXrPglo4IrVcqaoZFQ0nuMaVbieX
11+
0dDyTfsTaVQYYtqAtzhI/UGSIOhk2+lB9fP68jw9cLH0QYvR+qQ0IPG13I5zmSYP
12+
Mj0VYwMn9TF9/2sBOSRVgTVAcrYgOQLk3s/fGe66tmVBIWYcq65ygqD1+weu+Pax
13+
jPnwsefkdnf6JdYRG1F1Co7g52poPEYieAHfQOJ69sG0LYx0lBODC69qvSJ4WdCQ
14+
0zKw288CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEArr5r1UxgUzPykmu5ZdL6y8TA
15+
ZbSQ1yBY0nhsRwRkDd66iPK9U6T6K2+pL8Vc6ioov9WOtHQ6ohP3gSavd40cHRmF
16+
auwIsZ4Wk0mjftpOuPFp1hyo8d/QYrbEm3qNe5qln5S9h8ipoYvFtf5zlK2KHJFz
17+
9ehZMZ1zGAERNCVM8UUQKyUuZB5GyrZlbslf6P/9Bsc54YUWxP2pr5r/RJ6DeXfI
18+
zAFfXT8AFVlClARA949gpX0LVrXryDN60CUJ88QJmYCQ3AtIgzYYeqcdYHTd8eS2
19+
9P5whDdU5NvROP+LjETeReJF4Bfyc2gM7zxZD2BDSf5exvnNqiy42/lR1b4szw==
20+
-----END CERTIFICATE-----

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

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEpAIBAAKCAQEA2DGZ4eJVG4FPCUZDWgzYzVyRAIj+jiFhw70Fh3ENgeIDuPLV
3+
ZV6fKPwG1SLkCP1UOGl+K57ANqsZDlpOCAPWkyOTDQyisEoGv37W7XEYf/xDeWSB
4+
T45Bes+CWjgitVypqhkVDSe4xpVuJ5fR0PJN+xNpVBhi2oC3OEj9QZIg6GTb6UH1
5+
8/ryPD1wsfRBi9H6pDQg8bXcjnOZJg8yPRVjAyf1MX3/awE5JFWBNUBytiA5AuTe
6+
z98Z7rq2ZUEhZhyrrnKCoPX7B6749rGM+fCx5+R2d/ol1hEbUXUKjuDnamg8RiJ4
7+
Ad9A4nr2wbQtjHSUE4MLr2q9InhZ0JDTMrDbzwIDAQABAoIBADwJykpIqIny5xgU
8+
QzAG0U52nm4fnVGrQ5MwMxDh/HZNZes+xLRaCqk/FEasYdd9Qp5H7Zn/hDGqYlLy
9+
ESl4p2ZFQtkk4SlD5YvYladq+PrR+4sCtkZ5owWQCwsy+7CSAywRux7kIRRE+0pT
10+
hxkXsUBAq8eG3i0AAeHHo01KX4kptlJ5d1pFKKAPThTUHCT4VPHg8r59IdsNy6wC
11+
+0E5ZRWsVUePy+ERuarX/um896hgbaiDJLFk02Orlc87+OBmRwO8J+KoUOEcAiTO
12+
OZqGGaDEn5Y2mEdp2cCmq7+Izcklaha6CPsoV8+O2HK8PKvBIQmlgbDmal4/RNqr
13+
JFqYz0ECgYEA+5z74Tmj+tzH57lcdMqVpndG39N8spBe8JbiFL16qOb6gRDytXjc
14+
hY6IQo4JStpJulnPBZ5JQSbSBgCOzYWJJVBnnwMJKjNCd1th4znjxxMOe4LiDTtw
15+
D3hQtzBU9FlI2sjWEUKf1xCyi9N41ApQC5eDWWd/0GN9+xAsxRjLL00CgYEA2/aH
16+
4kNVsBHQ7vmv+sNsWeIgKg7PC7hRjcCYQG9ylBbBnFtv5XJYicXwqorqngzJPoGw
17+
gB7iaSWL1UNAOSWRSFYe+woPpkY7n6Pbq211nzqV1avAdVrLylJwyE+EOQgTS30D
18+
8BHv0I714PMd/QLK5NSUEr1IRtCfLeMpcSg6YYsCgYEAv3O86KxeTMTvyy9s3WVE
19+
p4y8vhUDHi/iPbjhQBzJF3nhhJGrzE+xpGJG5jWDdpRQY15wuvqtDMkIKA8GmfWQ
20+
3Hao0gKSV6z3VzCOdEKZQeILNAnsDVt7shm/eRRqoB7L48XLtQh37UJESUbY+qb6
21+
L0fTZxTs2VjLBF1TY4mxGUUCgYEA1PLENKnJkA5/fowd8aA2CoKfbvgtPARyd9Bn
22+
1aHPhEzPnabsGm7sBl2qFAEvCFoKjkgR7sd3nCHsUUetKmYTU7uEfLcN1YSS/oct
23+
CLaMs92M53JCfZqsRrAvXc2VjX0i6Ocb49QJnph4tBHKC4MjmAuxWr8C9QPNxyfv
24+
nAw9EOcCgYBYzejUzp6xiv5YzpwIncIF0A5E6VITcsW+LOR/FZOUPso0X2hQoIEs
25+
wx8HjKCGfvX6628vnaWJC099hTmOzVwpEgik5zOmeAmZ//gt2I53Yg/loQUzH0CD
26+
iXxrg/4Up7Yxx897w11ukOZv2xwmAFO1o52Q8k7d5FiMfEIzAkS3Pg==
27+
-----END RSA PRIVATE KEY-----

src/test/ssl/sslfiles.mk

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ SERVERS := server-cn-and-alt-names \
3333
server-multiple-alt-names \
3434
server-no-names \
3535
server-revoked
36-
CLIENTS := client client-dn client-revoked client_ext
36+
CLIENTS := client client-dn client-revoked client_ext client-long
3737

3838
#
3939
# To add a new non-standard key, add it to SPECIAL_KEYS and then add a recipe

src/test/ssl/t/001_ssltests.pl

+38-2
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,10 @@ sub switch_server_cert
683683
. sslkey('client-revoked.key'),
684684
"certificate authorization fails with revoked client cert",
685685
expected_stderr => qr/SSL error: sslv3 alert certificate revoked/,
686+
log_like => [
687+
qr{Client certificate verification failed at depth 0: certificate revoked},
688+
qr{Failed certificate data \(unverified\): subject "/CN=ssltestuser", serial number 2315134995201656577, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
689+
],
686690
# revoked certificates should not authenticate the user
687691
log_unlike => [qr/connection authenticated:/],);
688692

@@ -730,7 +734,35 @@ sub switch_server_cert
730734
$node->connect_fails(
731735
$common_connstr . " " . "sslmode=require sslcert=ssl/client.crt",
732736
"intermediate client certificate is missing",
733-
expected_stderr => qr/SSL error: tlsv1 alert unknown ca/);
737+
expected_stderr => qr/SSL error: tlsv1 alert unknown ca/,
738+
log_like => [
739+
qr{Client certificate verification failed at depth 0: unable to get local issuer certificate},
740+
qr{Failed certificate data \(unverified\): subject "/CN=ssltestuser", serial number 2315134995201656576, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
741+
]);
742+
743+
$node->connect_fails(
744+
"$common_connstr sslmode=require sslcert=ssl/client-long.crt " . sslkey('client-long.key'),
745+
"logged client certificate Subjects are truncated if they're too long",
746+
expected_stderr => qr/SSL error: tlsv1 alert unknown ca/,
747+
log_like => [
748+
qr{Client certificate verification failed at depth 0: unable to get local issuer certificate},
749+
qr{Failed certificate data \(unverified\): subject "\.\.\./CN=ssl-123456789012345678901234567890123456789012345678901234567890", serial number 2315418733629425152, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
750+
]);
751+
752+
# Use an invalid cafile here so that the next test won't be able to verify the
753+
# client CA.
754+
switch_server_cert($node, certfile => 'server-cn-only', cafile => 'server-cn-only');
755+
756+
# intermediate CA is provided but doesn't have a trusted root (checks error
757+
# logging for cert chain depths > 0)
758+
$node->connect_fails(
759+
"$common_connstr sslmode=require sslcert=ssl/client+client_ca.crt",
760+
"intermediate client certificate is untrusted",
761+
expected_stderr => qr/SSL error: tlsv1 alert unknown ca/,
762+
log_like => [
763+
qr{Client certificate verification failed at depth 1: unable to get local issuer certificate},
764+
qr{Failed certificate data \(unverified\): subject "/CN=Test CA for PostgreSQL SSL regression test client certs", serial number 2315134995201656577, issuer "/CN=Test root CA for PostgreSQL SSL regression test suite"},
765+
]);
734766

735767
# test server-side CRL directory
736768
switch_server_cert(
@@ -743,6 +775,10 @@ sub switch_server_cert
743775
"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt "
744776
. sslkey('client-revoked.key'),
745777
"certificate authorization fails with revoked client cert with server-side CRL directory",
746-
expected_stderr => qr/SSL error: sslv3 alert certificate revoked/);
778+
expected_stderr => qr/SSL error: sslv3 alert certificate revoked/,
779+
log_like => [
780+
qr{Client certificate verification failed at depth 0: certificate revoked},
781+
qr{Failed certificate data \(unverified\): subject "/CN=ssltestuser", serial number 2315134995201656577, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
782+
]);
747783

748784
done_testing();

src/test/ssl/t/SSL/Backend/OpenSSL.pm

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ sub init
8888
"client.key", "client-revoked.key",
8989
"client-der.key", "client-encrypted-pem.key",
9090
"client-encrypted-der.key", "client-dn.key",
91-
"client_ext.key");
91+
"client_ext.key", "client-long.key");
9292
foreach my $keyfile (@keys)
9393
{
9494
copy("ssl/$keyfile", "$cert_tempdir/$keyfile")

0 commit comments

Comments
 (0)