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
This commit is contained in:
Peter Eisentraut 2022-07-15 16:18:32 +02:00
parent 507ba16b28
commit 3a0e385048
7 changed files with 210 additions and 7 deletions

View File

@ -81,6 +81,9 @@ static bool ssl_is_server_start;
static int ssl_protocol_version_to_openssl(int v);
static const char *ssl_protocol_version_to_string(int v);
/* for passing data back from verify_cb() */
static const char *cert_errdetail;
/* ------------------------------------------------------------ */
/* Public interface */
/* ------------------------------------------------------------ */
@ -541,6 +544,7 @@ aloop:
(errcode(ERRCODE_PROTOCOL_VIOLATION),
errmsg("could not accept SSL connection: %s",
SSLerrmessage(ecode)),
cert_errdetail ? errdetail_internal("%s", cert_errdetail) : 0,
give_proto_hint ?
errhint("This may indicate that the client does not support any SSL protocol version between %s and %s.",
ssl_min_protocol_version ?
@ -549,6 +553,7 @@ aloop:
ssl_max_protocol_version ?
ssl_protocol_version_to_string(ssl_max_protocol_version) :
MAX_OPENSSL_TLS_VERSION) : 0));
cert_errdetail = NULL;
break;
case SSL_ERROR_ZERO_RETURN:
ereport(COMMERROR,
@ -1076,12 +1081,46 @@ dummy_ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata)
return 0;
}
/*
* Examines the provided certificate name, and if it's too long to log, modifies
* and truncates it. The return value is NULL if no truncation was needed; it
* otherwise points into the middle of the input string, and should not be
* freed.
*/
static char *
truncate_cert_name(char *name)
{
size_t namelen = strlen(name);
char *truncated;
/*
* Common Names are 64 chars max, so for a common case where the CN is the
* last field, we can still print the longest possible CN with a
* 7-character prefix (".../CN=[64 chars]"), for a reasonable limit of 71
* characters.
*/
#define MAXLEN 71
if (namelen <= MAXLEN)
return NULL;
/*
* Keep the end of the name, not the beginning, since the most specific
* field is likely to give users the most information.
*/
truncated = name + namelen - MAXLEN;
truncated[0] = truncated[1] = truncated[2] = '.';
#undef MAXLEN
return truncated;
}
/*
* Certificate verification callback
*
* This callback allows us to log intermediate problems during
* verification, but for now we'll see if the final error message
* contains enough information.
* This callback allows us to examine intermediate problems during
* verification, for later logging.
*
* This callback also allows us to override the default acceptance
* 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)
static int
verify_cb(int ok, X509_STORE_CTX *ctx)
{
int depth;
int errcode;
const char *errstring;
StringInfoData str;
X509 *cert;
if (ok)
{
/* Nothing to do for the successful case. */
return ok;
}
/* Pull all the information we have on the verification failure. */
depth = X509_STORE_CTX_get_error_depth(ctx);
errcode = X509_STORE_CTX_get_error(ctx);
errstring = X509_verify_cert_error_string(errcode);
initStringInfo(&str);
appendStringInfo(&str,
_("Client certificate verification failed at depth %d: %s."),
depth, errstring);
cert = X509_STORE_CTX_get_current_cert(ctx);
if (cert)
{
char *subject,
*issuer;
char *sub_truncated,
*iss_truncated;
char *serialno;
ASN1_INTEGER *sn;
BIGNUM *b;
/*
* Get the Subject and Issuer for logging, but don't let maliciously
* huge certs flood the logs.
*/
subject = X509_NAME_to_cstring(X509_get_subject_name(cert));
sub_truncated = truncate_cert_name(subject);
issuer = X509_NAME_to_cstring(X509_get_issuer_name(cert));
iss_truncated = truncate_cert_name(issuer);
/*
* Pull the serial number, too, in case a Subject is still ambiguous.
* This mirrors be_tls_get_peer_serial().
*/
sn = X509_get_serialNumber(cert);
b = ASN1_INTEGER_to_BN(sn, NULL);
serialno = BN_bn2dec(b);
appendStringInfoChar(&str, '\n');
appendStringInfo(&str,
_("Failed certificate data (unverified): subject \"%s\", serial number %s, issuer \"%s\"."),
sub_truncated ? sub_truncated : subject,
serialno ? serialno : _("unknown"),
iss_truncated ? iss_truncated : issuer);
BN_free(b);
OPENSSL_free(serialno);
pfree(issuer);
pfree(subject);
}
/* Store our detail message to be logged later. */
cert_errdetail = str.data;
return ok;
}

View File

@ -0,0 +1,14 @@
# An OpenSSL format CSR config file for creating a client certificate with a
# long Subject.
[ req ]
distinguished_name = req_distinguished_name
prompt = no
[ req_distinguished_name ]
# Common Names are 64 characters max
CN = ssl-123456789012345678901234567890123456789012345678901234567890
OU = Some Organizational Unit
O = PostgreSQL Global Development Group
# no extensions in client certs

View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDWjCCAkICCCAiBRIUREYAMA0GCSqGSIb3DQEBCwUAMEIxQDA+BgNVBAMMN1Rl
c3QgQ0EgZm9yIFBvc3RncmVTUUwgU1NMIHJlZ3Jlc3Npb24gdGVzdCBjbGllbnQg
Y2VydHMwHhcNMjIwNTEyMjE0NDQ3WhcNNDkwOTI3MjE0NDQ3WjCBnDEsMCoGA1UE
CgwjUG9zdGdyZVNRTCBHbG9iYWwgRGV2ZWxvcG1lbnQgR3JvdXAxITAfBgNVBAsM
GFNvbWUgT3JnYW5pemF0aW9uYWwgVW5pdDFJMEcGA1UEAwxAc3NsLTEyMzQ1Njc4
OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2
Nzg5MDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANgxmeHiVRuBTwlG
Q1oM2M1ckQCI/o4hYcO9BYdxDYHiA7jy1WVenyj8BtUi5Aj9VDhpfiuewDarGQ5a
TggD1pMjkw0MorBKBr9+1u1xGH/8Q3lkgU+OQXrPglo4IrVcqaoZFQ0nuMaVbieX
0dDyTfsTaVQYYtqAtzhI/UGSIOhk2+lB9fP68jw9cLH0QYvR+qQ0IPG13I5zmSYP
Mj0VYwMn9TF9/2sBOSRVgTVAcrYgOQLk3s/fGe66tmVBIWYcq65ygqD1+weu+Pax
jPnwsefkdnf6JdYRG1F1Co7g52poPEYieAHfQOJ69sG0LYx0lBODC69qvSJ4WdCQ
0zKw288CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEArr5r1UxgUzPykmu5ZdL6y8TA
ZbSQ1yBY0nhsRwRkDd66iPK9U6T6K2+pL8Vc6ioov9WOtHQ6ohP3gSavd40cHRmF
auwIsZ4Wk0mjftpOuPFp1hyo8d/QYrbEm3qNe5qln5S9h8ipoYvFtf5zlK2KHJFz
9ehZMZ1zGAERNCVM8UUQKyUuZB5GyrZlbslf6P/9Bsc54YUWxP2pr5r/RJ6DeXfI
zAFfXT8AFVlClARA949gpX0LVrXryDN60CUJ88QJmYCQ3AtIgzYYeqcdYHTd8eS2
9P5whDdU5NvROP+LjETeReJF4Bfyc2gM7zxZD2BDSf5exvnNqiy42/lR1b4szw==
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA2DGZ4eJVG4FPCUZDWgzYzVyRAIj+jiFhw70Fh3ENgeIDuPLV
ZV6fKPwG1SLkCP1UOGl+K57ANqsZDlpOCAPWkyOTDQyisEoGv37W7XEYf/xDeWSB
T45Bes+CWjgitVypqhkVDSe4xpVuJ5fR0PJN+xNpVBhi2oC3OEj9QZIg6GTb6UH1
8/ryPD1wsfRBi9H6pDQg8bXcjnOZJg8yPRVjAyf1MX3/awE5JFWBNUBytiA5AuTe
z98Z7rq2ZUEhZhyrrnKCoPX7B6749rGM+fCx5+R2d/ol1hEbUXUKjuDnamg8RiJ4
Ad9A4nr2wbQtjHSUE4MLr2q9InhZ0JDTMrDbzwIDAQABAoIBADwJykpIqIny5xgU
QzAG0U52nm4fnVGrQ5MwMxDh/HZNZes+xLRaCqk/FEasYdd9Qp5H7Zn/hDGqYlLy
ESl4p2ZFQtkk4SlD5YvYladq+PrR+4sCtkZ5owWQCwsy+7CSAywRux7kIRRE+0pT
hxkXsUBAq8eG3i0AAeHHo01KX4kptlJ5d1pFKKAPThTUHCT4VPHg8r59IdsNy6wC
+0E5ZRWsVUePy+ERuarX/um896hgbaiDJLFk02Orlc87+OBmRwO8J+KoUOEcAiTO
OZqGGaDEn5Y2mEdp2cCmq7+Izcklaha6CPsoV8+O2HK8PKvBIQmlgbDmal4/RNqr
JFqYz0ECgYEA+5z74Tmj+tzH57lcdMqVpndG39N8spBe8JbiFL16qOb6gRDytXjc
hY6IQo4JStpJulnPBZ5JQSbSBgCOzYWJJVBnnwMJKjNCd1th4znjxxMOe4LiDTtw
D3hQtzBU9FlI2sjWEUKf1xCyi9N41ApQC5eDWWd/0GN9+xAsxRjLL00CgYEA2/aH
4kNVsBHQ7vmv+sNsWeIgKg7PC7hRjcCYQG9ylBbBnFtv5XJYicXwqorqngzJPoGw
gB7iaSWL1UNAOSWRSFYe+woPpkY7n6Pbq211nzqV1avAdVrLylJwyE+EOQgTS30D
8BHv0I714PMd/QLK5NSUEr1IRtCfLeMpcSg6YYsCgYEAv3O86KxeTMTvyy9s3WVE
p4y8vhUDHi/iPbjhQBzJF3nhhJGrzE+xpGJG5jWDdpRQY15wuvqtDMkIKA8GmfWQ
3Hao0gKSV6z3VzCOdEKZQeILNAnsDVt7shm/eRRqoB7L48XLtQh37UJESUbY+qb6
L0fTZxTs2VjLBF1TY4mxGUUCgYEA1PLENKnJkA5/fowd8aA2CoKfbvgtPARyd9Bn
1aHPhEzPnabsGm7sBl2qFAEvCFoKjkgR7sd3nCHsUUetKmYTU7uEfLcN1YSS/oct
CLaMs92M53JCfZqsRrAvXc2VjX0i6Ocb49QJnph4tBHKC4MjmAuxWr8C9QPNxyfv
nAw9EOcCgYBYzejUzp6xiv5YzpwIncIF0A5E6VITcsW+LOR/FZOUPso0X2hQoIEs
wx8HjKCGfvX6628vnaWJC099hTmOzVwpEgik5zOmeAmZ//gt2I53Yg/loQUzH0CD
iXxrg/4Up7Yxx897w11ukOZv2xwmAFO1o52Q8k7d5FiMfEIzAkS3Pg==
-----END RSA PRIVATE KEY-----

View File

@ -33,7 +33,7 @@ SERVERS := server-cn-and-alt-names \
server-multiple-alt-names \
server-no-names \
server-revoked
CLIENTS := client client-dn client-revoked client_ext
CLIENTS := client client-dn client-revoked client_ext client-long
#
# To add a new non-standard key, add it to SPECIAL_KEYS and then add a recipe

View File

@ -683,6 +683,10 @@ $node->connect_fails(
. sslkey('client-revoked.key'),
"certificate authorization fails with revoked client cert",
expected_stderr => qr/SSL error: sslv3 alert certificate revoked/,
log_like => [
qr{Client certificate verification failed at depth 0: certificate revoked},
qr{Failed certificate data \(unverified\): subject "/CN=ssltestuser", serial number 2315134995201656577, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
],
# revoked certificates should not authenticate the user
log_unlike => [qr/connection authenticated:/],);
@ -730,7 +734,35 @@ $node->connect_ok(
$node->connect_fails(
$common_connstr . " " . "sslmode=require sslcert=ssl/client.crt",
"intermediate client certificate is missing",
expected_stderr => qr/SSL error: tlsv1 alert unknown ca/);
expected_stderr => qr/SSL error: tlsv1 alert unknown ca/,
log_like => [
qr{Client certificate verification failed at depth 0: unable to get local issuer certificate},
qr{Failed certificate data \(unverified\): subject "/CN=ssltestuser", serial number 2315134995201656576, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
]);
$node->connect_fails(
"$common_connstr sslmode=require sslcert=ssl/client-long.crt " . sslkey('client-long.key'),
"logged client certificate Subjects are truncated if they're too long",
expected_stderr => qr/SSL error: tlsv1 alert unknown ca/,
log_like => [
qr{Client certificate verification failed at depth 0: unable to get local issuer certificate},
qr{Failed certificate data \(unverified\): subject "\.\.\./CN=ssl-123456789012345678901234567890123456789012345678901234567890", serial number 2315418733629425152, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
]);
# Use an invalid cafile here so that the next test won't be able to verify the
# client CA.
switch_server_cert($node, certfile => 'server-cn-only', cafile => 'server-cn-only');
# intermediate CA is provided but doesn't have a trusted root (checks error
# logging for cert chain depths > 0)
$node->connect_fails(
"$common_connstr sslmode=require sslcert=ssl/client+client_ca.crt",
"intermediate client certificate is untrusted",
expected_stderr => qr/SSL error: tlsv1 alert unknown ca/,
log_like => [
qr{Client certificate verification failed at depth 1: unable to get local issuer certificate},
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"},
]);
# test server-side CRL directory
switch_server_cert(
@ -743,6 +775,10 @@ $node->connect_fails(
"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt "
. sslkey('client-revoked.key'),
"certificate authorization fails with revoked client cert with server-side CRL directory",
expected_stderr => qr/SSL error: sslv3 alert certificate revoked/);
expected_stderr => qr/SSL error: sslv3 alert certificate revoked/,
log_like => [
qr{Client certificate verification failed at depth 0: certificate revoked},
qr{Failed certificate data \(unverified\): subject "/CN=ssltestuser", serial number 2315134995201656577, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"},
]);
done_testing();

View File

@ -88,7 +88,7 @@ sub init
"client.key", "client-revoked.key",
"client-der.key", "client-encrypted-pem.key",
"client-encrypted-der.key", "client-dn.key",
"client_ext.key");
"client_ext.key", "client-long.key");
foreach my $keyfile (@keys)
{
copy("ssl/$keyfile", "$cert_tempdir/$keyfile")