From 99fc46ed0bf05eedbe7539890d946db472617150 Mon Sep 17 00:00:00 2001 From: David Zhang Date: Tue, 5 Mar 2024 15:31:22 -0800 Subject: [PATCH 1/3] 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 | 198 ++++++++++++++++++ src/interfaces/libpq/libpq-int.h | 1 + 8 files changed, 336 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 5612c29f8b..03ebc29e34 100644 --- a/src/backend/libpq/be-secure.c +++ b/src/backend/libpq/be-secure.c @@ -34,6 +34,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 45013582a7..48ba746614 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -4531,6 +4531,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 edcc0282b2..e7fe38afd9 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -116,6 +116,7 @@ #ssl = off #ssl_ca_file = '' #ssl_cert_file = 'server.crt' +#ssl_ocsp_file = 'server.res' #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 d4e10a0c4f..335da4df54 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)}, + {"sslocspstapling", "PGSSLOCSPSTAPLING", "0", NULL, + "SSL-OCSP-Stapling", "", 1, + offsetof(struct pg_conn, sslocspstapling)}, + /* * As with SSL, all GSS options are exposed even in builds that don't have * support. @@ -438,6 +442,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 */ @@ -1576,6 +1581,18 @@ pqConnectOptions2(PGconn *conn) return false; } + /* + * Validate sslocspstapling settings + */ + if (!sslVerifyOcspStapling(conn->sslocspstapling)) + { + conn->status = CONNECTION_BAD; + libpq_append_conn_error(conn, "invalid %s value: \"%s\"", + "sslocspstapling", + conn->sslocspstapling); + return false; + } + /* * validate sslcertmode option */ @@ -4388,6 +4405,7 @@ freePGconn(PGconn *conn) free(conn->require_auth); free(conn->ssl_min_protocol_version); free(conn->ssl_max_protocol_version); + free(conn->sslocspstapling); free(conn->gssencmode); free(conn->krbsrvname); free(conn->gsslib); @@ -7326,6 +7344,25 @@ sslVerifyProtocolRange(const char *min, const char *max) return true; } +/* + * Check sslocspstapling 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 8110882262..07f73980cb 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); @@ -95,6 +96,9 @@ static pthread_mutex_t ssl_config_mutex = PTHREAD_MUTEX_INITIALIZER; 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 */ @@ -1182,6 +1186,36 @@ initialize_SSL(PGconn *conn) have_cert = true; } + /* Enable OCSP stapling for certificate status check */ + if (conn->sslocspstapling && + strlen(conn->sslocspstapling) != 0 && + (strcmp(conn->sslocspstapling, "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 @@ -2043,3 +2077,167 @@ 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_data; + long resp_len = 0; + int resp_count = 0; + int cert_index = 0; + OCSP_RESPONSE *ocsp_resp = NULL; + OCSP_BASICRESP *basic_resp = NULL; + STACK_OF(X509) *peer_chain = NULL; + int ocsp_status = OCSP_CERT_STATUS_NOK; + X509 *ocsp_signer = NULL; + STACK_OF(X509) *ocsp_issuers = NULL; + X509_STORE *cert_store = NULL; + + /* + * step-1: retrieve OCSP response + */ + /* 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_data); + if (resp_data == NULL) + goto cleanup; /* no OCSP response */ + + /* convert OCSP response to internal format */ + ocsp_resp = d2i_OCSP_RESPONSE(NULL, &resp_data, resp_len); + if (ocsp_resp == NULL) + goto cleanup; /* failed to convert OCSP response */ + + /* + * step-2: verify the basic of OCSP response + */ + 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 */ + resp_count = OCSP_resp_count(basic_resp); + if (resp_count < 1) + goto cleanup; + + /* + * step-3: verify each revocation status + */ + OCSP_resp_get0_signer(basic_resp, &ocsp_signer, NULL); + if (ocsp_signer == NULL) + goto cleanup; + ocsp_issuers = sk_X509_new_null(); + if (ocsp_issuers == NULL) + goto cleanup; + if (!sk_X509_push(ocsp_issuers, ocsp_signer)) + goto cleanup; + + /* get certificate chain from peer */ + peer_chain = SSL_get_peer_cert_chain(ssl); + if (peer_chain == NULL) + goto cleanup; + + /* get local certificate store */ + cert_store = SSL_CTX_get_cert_store(SSL_get_SSL_CTX(ssl)); + if (cert_store == NULL) + goto cleanup; + + /* verify ocsp response signature */ + if (OCSP_basic_verify(basic_resp, ocsp_issuers, cert_store, OCSP_TRUSTOTHER) != 1) + goto cleanup; /* basic verification failed */ + + /* check each ocsp response status */ + for (cert_index = 0; cert_index < resp_count; cert_index ++) + { + X509 *my_cert; + X509 *my_issuer; + OCSP_CERTID *my_cid; + int my_status; + int rev_reason; + ASN1_GENERALIZEDTIME *rev_time; + ASN1_GENERALIZEDTIME *this_update; + ASN1_GENERALIZEDTIME *next_update; + + /* get certificate and issuer to construct ocsp status lookup id */ + my_cert = sk_X509_value(peer_chain, cert_index); + if (cert_index + 1 == resp_count) + { + X509_STORE_CTX *inctx = NULL; + X509_OBJECT *obj; + + inctx = X509_STORE_CTX_new(); + if (inctx == NULL) + goto cleanup; + if (!X509_STORE_CTX_init(inctx, cert_store, NULL, NULL)) + goto cleanup; + obj = X509_STORE_CTX_get_obj_by_subject(inctx, X509_LU_X509, X509_get_issuer_name(my_cert)); + if (obj == NULL) + goto cleanup; + + my_cid = OCSP_cert_to_id(NULL, my_cert, X509_OBJECT_get0_X509(obj)); + X509_OBJECT_free(obj); + } + else + { + my_issuer = sk_X509_value(peer_chain, cert_index + 1); + my_cid = OCSP_cert_to_id(NULL, my_cert, my_issuer); + } + + if (my_cid == NULL) + goto cleanup; + + /* verify ocsp status for given certificate */ + if (OCSP_resp_find_status(basic_resp, my_cid, + &my_status, &rev_reason, + &rev_time, &this_update, &next_update) != 1) + goto cleanup; /* internal error */ + + if (my_status == V_OCSP_CERTSTATUS_GOOD) + { + if (OCSP_check_validity(this_update, next_update, 0, -1) != 1) + goto cleanup; /* expired */ + continue; + } + else if (my_status == V_OCSP_CERTSTATUS_REVOKED) + goto cleanup; /* revoked */ + else if (my_status == V_OCSP_CERTSTATUS_UNKNOWN) + goto cleanup; /* unknown */ + } + if (cert_index == resp_count) + ocsp_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 ocsp_status; +} diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 82c18f870d..e65885cd39 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 *sslocspstapling; /* 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