From 5a7b9216869837d7de73dd3edd4827611b8cfe8b Mon Sep 17 00:00:00 2001 From: David Zhang Date: Tue, 20 Feb 2024 16:01:35 -0800 Subject: [PATCH] support certificate status check using OCSP stapling --- doc/src/sgml/config.sgml | 17 +++ doc/src/sgml/libpq.sgml | 30 ++++ doc/src/sgml/runtime.sgml | 6 + 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 | 142 ++++++++++++++++++ src/interfaces/libpq/libpq-int.h | 1 + 11 files changed, 333 insertions(+) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index ffd711b7f2..af502d5c0f 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1262,6 +1262,23 @@ include_dir 'conf.d' + + ssl_ocsp_file (string) + + ssl_ocsp_file configuration parameter + + + + + Specifies the name of the file containing the SSL server stapled + certificate status (OCSP Stapling). Relative paths are relative to the + data directory. This parameter can only be set in the + postgresql.conf file or on the server command line. + The default is empty, meaning no OCSP Stapling file is loaded. + + + + ssl_crl_file (string) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 1d8998efb2..a6457453f5 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -1761,6 +1761,21 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + + sslocspstapling + + + If set to 1, an SSL connection to the server + will send a certificate status request. libpq + will then refuse to connect if the server does not provide a valid OCSP + Stapling response. If set to 0 (default), + libpq will neither request the certificate + status nor check OCSP Stapling response. This option is only available if + PostgreSQL is compiled with SSL support. + + + + sslkey @@ -8323,6 +8338,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) + + + + PGSSLOCSPSTAPLING + + PGSSLOCSPSTAPLING behaves the same as the connection parameter. + + + @@ -8899,6 +8924,11 @@ ldap://ldap.acme.com/cn=dbserver,cn=hosts?pgconnectinfo?base?(objectclass=*) Windows). + + Stapled server certificate status (OCSP Stapling) will be checked, + if the parameter sslocspstapling is set to 1. + + The location of the root certificate file and the CRL can be changed by setting diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml index 64753d9c01..8704f1d3e5 100644 --- a/doc/src/sgml/runtime.sgml +++ b/doc/src/sgml/runtime.sgml @@ -2428,6 +2428,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433 sent to client to indicate server's identity + + + server certificate status stapled by ocsp responder + sent to client to indicate server certificate status + + ($PGDATA/server.key) server private key 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 70652f0a3f..eae2676e3b 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -4490,6 +4490,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 e10755972a..a022a0a543 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 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..1b0f92aade 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,111 @@ 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 ++) + { + ASN1_GENERALIZEDTIME *rev, *thisupd, *nextupd; + + 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, &rev, &thisupd, &nextupd) + == V_OCSP_CERTSTATUS_GOOD) + { + if (!OCSP_check_validity(thisupd, nextupd, 0, -1)) + break; /* check validity of thisUpdate and nextUpdate failed */ + + continue; /* status is good */ + } + else + { + break; /* status is revoked or unknown */ + } + } + 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 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