From 6eb920de994ed6822494f29c141ba913547f7ee4 Mon Sep 17 00:00:00 2001 From: David Zhang Date: Fri, 2 Feb 2024 10:11:43 -0800 Subject: [PATCH] support certificate status check using OCSP stapling --- src/backend/libpq/be-secure-openssl.c | 87 +++++++++++ src/backend/libpq/be-secure.c | 1 + src/backend/utils/misc/guc_tables.c | 10 ++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/libpq/libpq.h | 1 + src/interfaces/libpq/fe-connect.c | 37 +++++ src/interfaces/libpq/fe-secure-openssl.c | 137 ++++++++++++++++++ src/interfaces/libpq/libpq-int.h | 1 + 8 files changed, 275 insertions(+) diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c index e12b1cc9e3..c727634dfa 100644 --- a/src/backend/libpq/be-secure-openssl.c +++ b/src/backend/libpq/be-secure-openssl.c @@ -50,6 +50,7 @@ #include #endif #include +#include /* default init hook can be overridden by a shared library */ @@ -81,6 +82,8 @@ 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); +static int ocsp_stapling_cb(SSL *ssl); + /* for passing data back from verify_cb() */ static const char *cert_errdetail; @@ -429,6 +432,9 @@ be_tls_open_server(Port *port) return -1; } + /* set up OCSP stapling callback */ + SSL_CTX_set_tlsext_status_cb(SSL_context, ocsp_stapling_cb); + /* set up debugging/info callback */ SSL_CTX_set_info_callback(SSL_context, info_cb); @@ -1653,3 +1659,84 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart) SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb); } } + +/* + * OCSP stapling callback function for the server side. + * + * This function is responsible for providing the OCSP stapling response to + * the client during the SSL/TLS handshake, based on the client's request. + * + * Parameters: + * - ssl: SSL/TLS connection object. + * + * Returns: + * - SSL_TLSEXT_ERR_OK: OCSP stapling response successfully provided. + * - SSL_TLSEXT_ERR_NOACK: OCSP stapling response not provided due to errors. + * + * Steps: + * 1. Check if the server-side OCSP stapling feature is enabled. + * 2. Read OCSP response from file if client requested OCSP stapling. + * 3. Set the OCSP stapling response in the SSL/TLS connection. + */ +static int ocsp_stapling_cb(SSL *ssl) +{ + int resp_len = -1; + BIO *bio = NULL; + OCSP_RESPONSE *resp = NULL; + unsigned char *rspder = NULL; + + /* return, if ssl_ocsp_file not enabled on server */ + if (ssl_ocsp_file == NULL) + { + ereport(WARNING, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not find ssl_ocsp_file"))); + return SSL_TLSEXT_ERR_NOACK; + } + + /* whether the client requested OCSP stapling */ + if (SSL_get_tlsext_status_type(ssl) == TLSEXT_STATUSTYPE_ocsp) + { + bio = BIO_new_file(ssl_ocsp_file, "r"); + if (bio == NULL) + { + ereport(WARNING, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not read ssl_ocsp_file"))); + return SSL_TLSEXT_ERR_NOACK; + } + + resp = d2i_OCSP_RESPONSE_bio(bio, NULL); + BIO_free(bio); + if (resp == NULL) + { + ereport(WARNING, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("could not convert OCSP response to intarnal format"))); + return SSL_TLSEXT_ERR_NOACK; + } + + resp_len = i2d_OCSP_RESPONSE(resp, &rspder); + OCSP_RESPONSE_free(resp); + if (resp_len <= 0) + { + ereport(WARNING, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("could not convert OCSP response to der format"))); + return SSL_TLSEXT_ERR_NOACK; + } + + /* set up the OCSP stapling response */ + if (SSL_set_tlsext_status_ocsp_resp(ssl, rspder, resp_len) != 1) + { + ereport(WARNING, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("could not set up OCSP stapling response"))); + return SSL_TLSEXT_ERR_NOACK; + } + + return SSL_TLSEXT_ERR_OK; + } + + return SSL_TLSEXT_ERR_NOACK; +} diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c index 6923c241b9..c57ea4ff33 100644 --- a/src/backend/libpq/be-secure.c +++ b/src/backend/libpq/be-secure.c @@ -37,6 +37,7 @@ char *ssl_library; char *ssl_cert_file; +char *ssl_ocsp_file; char *ssl_key_file; char *ssl_ca_file; char *ssl_crl_file; diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 7fe58518d7..83a49c6144 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -4479,6 +4479,16 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"ssl_ocsp_file", PGC_SIGHUP, CONN_AUTH_SSL, + gettext_noop("Location of the SSL certificate OCSP stapling file."), + NULL + }, + &ssl_ocsp_file, + "", + NULL, NULL, NULL + }, + { {"synchronous_standby_names", PGC_SIGHUP, REPLICATION_PRIMARY, gettext_noop("Number of synchronous standbys and list of names of potential synchronous ones."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index da10b43dac..608dead52b 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -107,6 +107,7 @@ #ssl = off #ssl_ca_file = '' #ssl_cert_file = 'server.crt' +#ssl_ocsp_file = 'server.resp' #ssl_crl_file = '' #ssl_crl_dir = '' #ssl_key_file = 'server.key' diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h index 6171a0d17a..edef65fd39 100644 --- a/src/include/libpq/libpq.h +++ b/src/include/libpq/libpq.h @@ -89,6 +89,7 @@ extern bool pq_check_connection(void); */ extern PGDLLIMPORT char *ssl_library; extern PGDLLIMPORT char *ssl_cert_file; +extern PGDLLIMPORT char *ssl_ocsp_file; extern PGDLLIMPORT char *ssl_key_file; extern PGDLLIMPORT char *ssl_ca_file; extern PGDLLIMPORT char *ssl_crl_file; diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index c0dea144a0..e8878df4fb 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -324,6 +324,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "SSL-Maximum-Protocol-Version", "", 8, /* sizeof("TLSv1.x") == 8 */ offsetof(struct pg_conn, ssl_max_protocol_version)}, + {"ssl_ocsp_stapling", "PGSSLOCSPSTAPLING", "0", NULL, + "SSL-OCSP-Stapling", "", 1, + offsetof(struct pg_conn, ssl_ocsp_stapling)}, + /* * As with SSL, all GSS options are exposed even in builds that don't have * support. @@ -443,6 +447,7 @@ static void pgpassfileWarning(PGconn *conn); static void default_threadlock(int acquire); static bool sslVerifyProtocolVersion(const char *version); static bool sslVerifyProtocolRange(const char *min, const char *max); +static bool sslVerifyOcspStapling(const char *stapling); /* global variable because fe-auth.c needs to access it */ @@ -1581,6 +1586,18 @@ connectOptions2(PGconn *conn) return false; } + /* + * Validate ssl_ocsp_stapling settings + */ + if (!sslVerifyOcspStapling(conn->ssl_ocsp_stapling)) + { + conn->status = CONNECTION_BAD; + libpq_append_conn_error(conn, "invalid %s value: \"%s\"", + "ssl_ocsp_stapling", + conn->ssl_ocsp_stapling); + return false; + } + /* * validate sslcertmode option */ @@ -4405,6 +4422,7 @@ freePGconn(PGconn *conn) free(conn->require_auth); free(conn->ssl_min_protocol_version); free(conn->ssl_max_protocol_version); + free(conn->ssl_ocsp_stapling); free(conn->gssencmode); free(conn->krbsrvname); free(conn->gsslib); @@ -7318,6 +7336,25 @@ sslVerifyProtocolRange(const char *min, const char *max) return true; } +/* + * Check ssl_ocsp_stapling is set properly + */ +static bool +sslVerifyOcspStapling(const char *stapling) +{ + /* + * An empty string or a NULL value is considered valid + */ + if (!stapling || strlen(stapling) == 0) + return true; + + if (pg_strcasecmp(stapling, "0") == 0 || + pg_strcasecmp(stapling, "1") == 0) + return true; + + /* anything else is wrong */ + return false; +} /* * Obtain user's home directory, return in given buffer diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index 6bc216956d..aa0b1bf130 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -62,6 +62,7 @@ #include #endif #include +#include static int verify_cb(int ok, X509_STORE_CTX *ctx); @@ -100,6 +101,9 @@ static long win32_ssl_create_mutex = 0; static PQsslKeyPassHook_OpenSSL_type PQsslKeyPassHook = NULL; static int ssl_protocol_version_to_openssl(const char *protocol); +static int ocsp_response_check_cb(SSL *ssl); +#define OCSP_CERT_STATUS_OK 1 +#define OCSP_CERT_STATUS_NOK (-1) /* ------------------------------------------------------------ */ /* Procedures common to all secure sessions */ @@ -1202,6 +1206,36 @@ initialize_SSL(PGconn *conn) have_cert = true; } + /* Enable OCSP stapling for certificate status check */ + if (conn->ssl_ocsp_stapling && + strlen(conn->ssl_ocsp_stapling) != 0 && + (strcmp(conn->ssl_ocsp_stapling, "1") == 0)) + { + /* set up certificate status request */ + if (SSL_CTX_set_tlsext_status_type(SSL_context, + TLSEXT_STATUSTYPE_ocsp) != 1) + { + char *err = SSLerrmessage(ERR_get_error()); + libpq_append_conn_error(conn, + "could not set up certificate status request: %s", err); + SSLerrfree(err); + SSL_CTX_free(SSL_context); + return -1; + } + + /* set up OCSP response callback */ + if (SSL_CTX_set_tlsext_status_cb(SSL_context, + ocsp_response_check_cb) <= 0) + { + char *err = SSLerrmessage(ERR_get_error()); + libpq_append_conn_error(conn, + "could not set up OCSP response callback: %s", err); + SSLerrfree(err); + SSL_CTX_free(SSL_context); + return -1; + } + } + /* * The SSL context is now loaded with the correct root and client * certificates. Create a connection-specific SSL object. The private key @@ -2063,3 +2097,106 @@ ssl_protocol_version_to_openssl(const char *protocol) return -1; } + + +/* + * Verify OCSP stapling response in the context of an SSL/TLS connection. + * + * This function checks whether the server provided an OCSP response + * as part of the TLS handshake, verifies its integrity, and checks the + * revocation status of the presented certificates. + * + * Parameters: + * - ssl: SSL/TLS connection object. + * + * Returns: + * - OCSP_CERT_STATUS_OK: OCSP stapling was not requested or status is OK. + * - OCSP_CERT_STATUS_NOK: OCSP verification failed or status is not OK. + * + * Steps: + * 1. Retrieve OCSP response during handshake. + * 2. Perform basic OCSP response verification. + * 3. Verify each revocation status in the OCSP response. + * + * Cleanup: + * - Free allocated memory for the OCSP response and basic response. + */ +static int ocsp_response_check_cb(SSL *ssl) +{ + const unsigned char *resp; + long resp_len = 0; + int cert_index = 0; + int num_of_resp = 0; + OCSP_RESPONSE *ocsp_resp = NULL; + OCSP_BASICRESP *basic_resp = NULL; + OCSP_SINGLERESP *single_resp = NULL; + int status = OCSP_CERT_STATUS_NOK; + + /* + * step-1: retrieve OCSP response + * refer to 'ocsp_resp_cb' in openssl/apps/s_client.c + */ + /* check if requested certificate status */ + if (SSL_get_tlsext_status_type(ssl) != TLSEXT_STATUSTYPE_ocsp) + return OCSP_CERT_STATUS_OK; + + /* check if got OCSP response */ + resp_len = SSL_get_tlsext_status_ocsp_resp(ssl, &resp); + if (resp == NULL) + goto cleanup; /* no OCSP response */ + + /* convert OCSP response to internal format */ + ocsp_resp = d2i_OCSP_RESPONSE(NULL, &resp, resp_len); + if (ocsp_resp == NULL) + goto cleanup; /* failed to convert OCSP response */ + + /* + * step-2: verify the basic of OCSP response + * refer to 'ocsp_main' in openssl/apps/ocsp.c + */ + if (OCSP_response_status(ocsp_resp) != OCSP_RESPONSE_STATUS_SUCCESSFUL) + goto cleanup; /* OCSP response not successful */ + + /* get OCSP basic response structure */ + basic_resp = OCSP_response_get1_basic(ocsp_resp); + if (basic_resp == NULL) + goto cleanup; /* failed to get basic OCSP response */ + + /* perform basic OCSP response verify */ + if (OCSP_basic_verify(basic_resp, SSL_get_peer_cert_chain(ssl), + SSL_CTX_get_cert_store(SSL_get_SSL_CTX(ssl)), 0) != 1) + goto cleanup; /* basic verification failed */ + + /* + * step-3: verify each revocation status + * ref to 'ct_extract_ocsp_response_scts' in openssl/ssl/ssl_lib.c + */ + num_of_resp = OCSP_resp_count(basic_resp); + for (cert_index = 0; cert_index < num_of_resp; cert_index ++) + { + single_resp = OCSP_resp_get0(basic_resp, cert_index); + if (single_resp == NULL) + goto cleanup; /* failed to get single response */ + + if (OCSP_single_get0_status(single_resp, NULL, NULL, NULL, NULL) + == V_OCSP_CERTSTATUS_GOOD) + continue; /* status is good */ + else + { + /* status is revoked or unknown */ + status = OCSP_CERT_STATUS_NOK; + break; + } + } + if (cert_index == num_of_resp) + status = OCSP_CERT_STATUS_OK; + +cleanup: + if (ocsp_resp != NULL) + OCSP_RESPONSE_free(ocsp_resp); + + if (basic_resp != NULL) + OCSP_BASICRESP_free(basic_resp); + + return status; +} diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index ff8e0dce77..a2ac07b64d 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -405,6 +405,7 @@ struct pg_conn char *gssdelegation; /* Try to delegate GSS credentials? (0 or 1) */ char *ssl_min_protocol_version; /* minimum TLS protocol version */ char *ssl_max_protocol_version; /* maximum TLS protocol version */ + char *ssl_ocsp_stapling; /* request ocsp stapling from server */ char *target_session_attrs; /* desired session properties */ char *require_auth; /* name of the expected auth method */ char *load_balance_hosts; /* load balance over hosts */ -- 2.34.1