Proposal for implementing OCSP Stapling in PostgreSQL
Hello PostgreSQL Hackers,
This proposal suggests implementing OCSP Stapling in PostgreSQL as an
alternative and more efficient method for checking certificate
revocation, aligning with the trend shift from Certificate Revocation
Lists (CRL).
1. benefits
OCSP Stapling offers several advantages over traditional CRL checks,
including:
*) enhances user trust and real-time certificate verification without
relying on potentially outdated CRLs.
*) helps address privacy concerns associated with traditional OCSP
checks, where the client contacts the OCSP responder directly.
*) reduces latency by eliminating the need for the client to perform an
additional round-trip to the OCSP responder.
*) efficient resource utilization by allowing the server to cache and
reuse OCSP responses.
2. a POC patch with below changes:
*) a new configuration option 'ssl_ocsp_file' to enable/disable OCSP
Stapling and specify OCSP responses for PostgreSQL servers. For
instance, ssl_ocsp_file = '_server.resp'
*) a server-side callback function responsible for generating OCSP
stapling responses. This function comes into play only when a client
requests the server's certificate status during the SSL/TLS handshake.
*) a new connection parameter 'ssl_ocsp_stapling' on the client side.
For example, when 'ssl_ocsp_stapling=1', the psql client will send a
certificate status request to the PostgreSQL server.
*) a client-side callback function within the libpq interface to
validate and check the stapled OCSP response received from the server.
If the server's certificate status is valid, the TLS handshake
continues; otherwise, the connection is rejected.
3. test cases for 'make check' are not yet ready as they could be
complicated, but basic tests can be performed as demonstrated below:
To run the tests, OpenSSL tools are required to simulate the OCSP
responder for generating OCSP responses. Additionally, initial
certificate generation, including a self-signed root CA, OCSP response
signing certificate, and PostgreSQL server certificate, is needed.
*) add ocsp atrributes to openssl.cnf
$ openssl version
OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)
$ diff openssl-ocsp.cnf /etc/ssl/openssl.cnf
204d203
< authorityInfoAccess = OCSP;URI:http://127.0.0.1:6655
232,235d230
< [ v3_ocsp ]
< basicConstraints = CA:FALSE
< keyUsage = nonRepudiation, digitalSignature, keyEncipherment
< extendedKeyUsage = OCSPSigning
255c250
< keyUsage = critical, cRLSign, digitalSignature, keyCertSign
---
*) prepare OCSP responder for generating OCSP response
$ mkdir -p demoCA/newcerts
$ touch demoCA/index.txt
$ echo '01' > demoCA/serial
# create a self-signed root CA
$ openssl req -new -nodes -out rootCA.csr -keyout rootCA.key -subj
"/C=CA/ST=BC/L=VAN/O=IDO/OU=DEV/CN=rootCA"
$ openssl x509 -req -in rootCA.csr -days 3650 -extfile openssl-ocsp.cnf
-extensions v3_ca -signkey rootCA.key -out rootCA.crt
# create a certificate for OCSP responder
$ openssl req -new -nodes -out ocspSigning.csr -keyout ocspSigning.key
-subj "/C=CA/ST=BC/L=VAN/O=IDO/OU=DEV/CN=ocspSigner"
$ openssl ca -keyfile rootCA.key -cert rootCA.crt -in ocspSigning.csr
-out ocspSigning.crt -config openssl-ocsp.cnf -extensions v3_ocsp
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y
# create a certificate for PostgreSQL server
$ openssl req -new -nodes -out server.csr -keyout server.key -subj
"/C=CA/ST=BC/L=VAN/O=IDO/OU=DEV/CN=server"
$ openssl ca -batch -days 365 -keyfile rootCA.key -cert rootCA.crt
-config openssl-ocsp.cnf -out server.crt -infiles server.csr
# start OCSP responder
$ openssl ocsp -index demoCA/index.txt -port 6655 -rsigner
ocspSigning.crt -rkey ocspSigning.key -CA rootCA.crt -text
# make sure PostgreSQL server's certificate is 'good'
$ openssl ocsp -issuer rootCA.crt -url http://127.0.0.1:6655 -resp_text
-noverify -cert server.crt
# generate OCSP response when certificate status is 'good' and save as
_server.resp-good:
$ openssl ocsp -issuer rootCA.crt -cert server.crt -url
http://127.0.0.1:6655 -respout _server.resp-good
# revoke PostgreSQL server's certificate
$ openssl ca -keyfile rootCA.key -cert rootCA.crt -revoke server.crt
# make sure PostgreSQL server's certificate is 'revoked'
$ openssl ocsp -issuer rootCA.crt -url http://127.0.0.1:6655 -resp_text
-noverify -cert server.crt
### generate OCSP response when certificate status is 'revoked' and save
as _server.resp-revoked:
$ openssl ocsp -issuer rootCA.crt -cert server.crt -url
http://127.0.0.1:6655 -respout _server.resp-revoked
*) setup OCSP stapling on PostgreSQL server side
copy 'rootCA.crt, server.key, server.crt, _server.resp-good, and
_server.resp-revoked' to pgdata folder and update PostgreSQL server
configuration by specifying ssl_ocsp_file = '_server.resp', where
'_server.resp' is either a copy of '_server.resp-good' or
'_server.resp-revoked' depending on the test case, for example:
listen_addresses = '*'
ssl = on
ssl_ca_file = 'rootCA.crt'
ssl_cert_file = 'server.crt'
ssl_key_file = 'server.key'
ssl_ocsp_file = '_server.resp'
*) test with psql client
3.1) PostgreSQL server's certificate status is 'good'
$ cp -pr _server.resp-good _server.resp
$ psql -d "sslmode=verify-ca sslrootcert=rootCA.crt user=david
dbname=postgres ssl_ocsp_stapling=1" -h 127.0.0.1 -p 5432
psql (17devel)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384,
compression: off)
Type "help" for help.
postgres=#
3.2) PostgreSQL server's certificate status is 'revoked'
$ cp -pr _server.resp-revoked _server.resp
$ psql -d "sslmode=verify-ca sslrootcert=rootCA.crt user=david
dbname=postgres ssl_ocsp_stapling=1" -h 127.0.0.1 -p 5432
psql: error: connection to server at "127.0.0.1", port 5432 failed: SSL
error: ocsp callback failure
3.3) PostgreSQL server's certificate status is 'revoked' but OCSP
stapling is not required by psql client:
$ psql -d "sslmode=verify-ca sslrootcert=rootCA.crt user=david
dbname=postgres ssl_ocsp_stapling=0" -h 127.0.0.1 -p 5432
psql (17devel)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384,
compression: off)
Type "help" for help.
postgres=#
This is a highly experimental proof of concept, and any comments or
feedback would be greatly appreciated!
Best regards,
David Zhang
===============
Highgo Software Canada
www.highgo.ca
Attachments:
v1-0001-support-certificate-status-check-using-OCSP-stapling.patchtext/plain; charset=UTF-8; name=v1-0001-support-certificate-status-check-using-OCSP-stapling.patchDownload
From 6eb920de994ed6822494f29c141ba913547f7ee4 Mon Sep 17 00:00:00 2001
From: David Zhang <idrawone@gmail.com>
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 <openssl/ec.h>
#endif
#include <openssl/x509v3.h>
+#include <openssl/ocsp.h>
/* 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 <openssl/engine.h>
#endif
#include <openssl/x509v3.h>
+#include <openssl/ocsp.h>
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
Hi Hackers,
This is the 2nd version patch with following updates:
1) Changed the frontend SSL parameter from `ssl_ocsp_stapling` to
`sslocspstapling` to align with other SSL parameters.
2) Documented both the backend parameter `ssl_ocsp_file` and the
frontend parameter `sslocspstapling`.
3) Implemented a check for the `nextUpdate` field in the OCSP response.
If it is present but expired, the TLS connection will fail."
To add the test cases for OCSP Stapling, I think I should 1) add one
section to conf/cas.config to generate `ocsp_ca.crt`; 2) use this
`ocsp_ca` to sign some OCSP responses for server side certificates with
`good`, `revoked` and `unknown`, and then 3) add some test cases to
t/001_ssltests.pl.
Any comments or feedback would be greatly appreciated!
Thank you,
David
Show quoted text
On 2024-02-05 3:51 p.m., David Zhang wrote:
Hello PostgreSQL Hackers,
This proposal suggests implementing OCSP Stapling in PostgreSQL as an
alternative and more efficient method for checking certificate
revocation, aligning with the trend shift from Certificate Revocation
Lists (CRL).1. benefits
OCSP Stapling offers several advantages over traditional CRL checks,
including:*) enhances user trust and real-time certificate verification without
relying on potentially outdated CRLs.
*) helps address privacy concerns associated with traditional OCSP
checks, where the client contacts the OCSP responder directly.
*) reduces latency by eliminating the need for the client to perform
an additional round-trip to the OCSP responder.
*) efficient resource utilization by allowing the server to cache and
reuse OCSP responses.2. a POC patch with below changes:
*) a new configuration option 'ssl_ocsp_file' to enable/disable OCSP
Stapling and specify OCSP responses for PostgreSQL servers. For
instance, ssl_ocsp_file = '_server.resp'*) a server-side callback function responsible for generating OCSP
stapling responses. This function comes into play only when a client
requests the server's certificate status during the SSL/TLS handshake.*) a new connection parameter 'ssl_ocsp_stapling' on the client side.
For example, when 'ssl_ocsp_stapling=1', the psql client will send a
certificate status request to the PostgreSQL server.*) a client-side callback function within the libpq interface to
validate and check the stapled OCSP response received from the server.
If the server's certificate status is valid, the TLS handshake
continues; otherwise, the connection is rejected.3. test cases for 'make check' are not yet ready as they could be
complicated, but basic tests can be performed as demonstrated below:
To run the tests, OpenSSL tools are required to simulate the OCSP
responder for generating OCSP responses. Additionally, initial
certificate generation, including a self-signed root CA, OCSP response
signing certificate, and PostgreSQL server certificate, is needed.*) add ocsp atrributes to openssl.cnf
$ openssl version
OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)$ diff openssl-ocsp.cnf /etc/ssl/openssl.cnf
204d203
< authorityInfoAccess = OCSP;URI:http://127.0.0.1:6655
232,235d230
< [ v3_ocsp ]
< basicConstraints = CA:FALSE
< keyUsage = nonRepudiation, digitalSignature, keyEncipherment
< extendedKeyUsage = OCSPSigning
255c250
< keyUsage = critical, cRLSign, digitalSignature, keyCertSign
---*) prepare OCSP responder for generating OCSP response
$ mkdir -p demoCA/newcerts
$ touch demoCA/index.txt
$ echo '01' > demoCA/serial# create a self-signed root CA
$ openssl req -new -nodes -out rootCA.csr -keyout rootCA.key -subj
"/C=CA/ST=BC/L=VAN/O=IDO/OU=DEV/CN=rootCA"
$ openssl x509 -req -in rootCA.csr -days 3650 -extfile
openssl-ocsp.cnf -extensions v3_ca -signkey rootCA.key -out rootCA.crt# create a certificate for OCSP responder
$ openssl req -new -nodes -out ocspSigning.csr -keyout ocspSigning.key
-subj "/C=CA/ST=BC/L=VAN/O=IDO/OU=DEV/CN=ocspSigner"
$ openssl ca -keyfile rootCA.key -cert rootCA.crt -in ocspSigning.csr
-out ocspSigning.crt -config openssl-ocsp.cnf -extensions v3_ocsp
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y# create a certificate for PostgreSQL server
$ openssl req -new -nodes -out server.csr -keyout server.key -subj
"/C=CA/ST=BC/L=VAN/O=IDO/OU=DEV/CN=server"
$ openssl ca -batch -days 365 -keyfile rootCA.key -cert rootCA.crt
-config openssl-ocsp.cnf -out server.crt -infiles server.csr# start OCSP responder
$ openssl ocsp -index demoCA/index.txt -port 6655 -rsigner
ocspSigning.crt -rkey ocspSigning.key -CA rootCA.crt -text# make sure PostgreSQL server's certificate is 'good'
$ openssl ocsp -issuer rootCA.crt -url http://127.0.0.1:6655
-resp_text -noverify -cert server.crt# generate OCSP response when certificate status is 'good' and save as
_server.resp-good:
$ openssl ocsp -issuer rootCA.crt -cert server.crt -url
http://127.0.0.1:6655 -respout _server.resp-good# revoke PostgreSQL server's certificate
$ openssl ca -keyfile rootCA.key -cert rootCA.crt -revoke server.crt# make sure PostgreSQL server's certificate is 'revoked'
$ openssl ocsp -issuer rootCA.crt -url http://127.0.0.1:6655
-resp_text -noverify -cert server.crt### generate OCSP response when certificate status is 'revoked' and
save as _server.resp-revoked:
$ openssl ocsp -issuer rootCA.crt -cert server.crt -url
http://127.0.0.1:6655 -respout _server.resp-revoked*) setup OCSP stapling on PostgreSQL server side
copy 'rootCA.crt, server.key, server.crt, _server.resp-good, and
_server.resp-revoked' to pgdata folder and update PostgreSQL server
configuration by specifying ssl_ocsp_file = '_server.resp', where
'_server.resp' is either a copy of '_server.resp-good' or
'_server.resp-revoked' depending on the test case, for example:listen_addresses = '*'
ssl = on
ssl_ca_file = 'rootCA.crt'
ssl_cert_file = 'server.crt'
ssl_key_file = 'server.key'
ssl_ocsp_file = '_server.resp'*) test with psql client
3.1) PostgreSQL server's certificate status is 'good'
$ cp -pr _server.resp-good _server.resp
$ psql -d "sslmode=verify-ca sslrootcert=rootCA.crt user=david
dbname=postgres ssl_ocsp_stapling=1" -h 127.0.0.1 -p 5432
psql (17devel)
SSL connection (protocol: TLSv1.2, cipher:
ECDHE-RSA-AES256-GCM-SHA384, compression: off)
Type "help" for help.postgres=#
3.2) PostgreSQL server's certificate status is 'revoked'
$ cp -pr _server.resp-revoked _server.resp
$ psql -d "sslmode=verify-ca sslrootcert=rootCA.crt user=david
dbname=postgres ssl_ocsp_stapling=1" -h 127.0.0.1 -p 5432
psql: error: connection to server at "127.0.0.1", port 5432 failed:
SSL error: ocsp callback failure3.3) PostgreSQL server's certificate status is 'revoked' but OCSP
stapling is not required by psql client:
$ psql -d "sslmode=verify-ca sslrootcert=rootCA.crt user=david
dbname=postgres ssl_ocsp_stapling=0" -h 127.0.0.1 -p 5432
psql (17devel)
SSL connection (protocol: TLSv1.2, cipher:
ECDHE-RSA-AES256-GCM-SHA384, compression: off)
Type "help" for help.postgres=#
This is a highly experimental proof of concept, and any comments or
feedback would be greatly appreciated!Best regards,
David Zhang===============
Highgo Software Canada
www.highgo.ca
Attachments:
v2-0001-support-certificate-status-check-using-OCSP-stapling.patchtext/plain; charset=UTF-8; name=v2-0001-support-certificate-status-check-using-OCSP-stapling.patchDownload
From 5a7b9216869837d7de73dd3edd4827611b8cfe8b Mon Sep 17 00:00:00 2001
From: David Zhang <idrawone@gmail.com>
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'
</listitem>
</varlistentry>
+ <varlistentry id="guc-ssl-ocsp-file" xreflabel="ssl_ocsp_file">
+ <term><varname>ssl_ocsp_file</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>ssl_ocsp_file</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ 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
+ <filename>postgresql.conf</filename> file or on the server command line.
+ The default is empty, meaning no OCSP Stapling file is loaded.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-ssl-crl-file" xreflabel="ssl_crl_file">
<term><varname>ssl_crl_file</varname> (<type>string</type>)
<indexterm>
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
</listitem>
</varlistentry>
+ <varlistentry id="libpq-connect-sslocspstapling" xreflabel="sslocspstapling">
+ <term><literal>sslocspstapling</literal></term>
+ <listitem>
+ <para>
+ If set to 1, an <acronym>SSL</acronym> connection to the server
+ will send a certificate status request. <application>libpq</application>
+ will then refuse to connect if the server does not provide a valid OCSP
+ Stapling response. If set to 0 (default),
+ <application>libpq</application> will neither request the certificate
+ status nor check OCSP Stapling response. This option is only available if
+ <productname>PostgreSQL</productname> is compiled with SSL support.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-connect-sslkey" xreflabel="sslkey">
<term><literal>sslkey</literal></term>
<listitem>
@@ -8323,6 +8338,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
</para>
</listitem>
+ <listitem>
+ <para>
+ <indexterm>
+ <primary><envar>PGSSLOCSPSTAPLING</envar></primary>
+ </indexterm>
+ <envar>PGSSLOCSPSTAPLING</envar> behaves the same as the <xref
+ linkend="libpq-connect-sslocspstapling"/> connection parameter.
+ </para>
+ </listitem>
+
<listitem>
<para>
<indexterm>
@@ -8899,6 +8924,11 @@ ldap://ldap.acme.com/cn=dbserver,cn=hosts?pgconnectinfo?base?(objectclass=*)
Windows).
</para>
+ <para>
+ Stapled server certificate status (OCSP Stapling) will be checked,
+ if the parameter <literal>sslocspstapling</literal> is set to 1.
+ </para>
+
<para>
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
<entry>sent to client to indicate server's identity</entry>
</row>
+ <row>
+ <entry><xref linkend="guc-ssl-ocsp-file"/></entry>
+ <entry>server certificate status stapled by ocsp responder</entry>
+ <entry>sent to client to indicate server certificate status</entry>
+ </row>
+
<row>
<entry><xref linkend="guc-ssl-key-file"/> (<filename>$PGDATA/server.key</filename>)</entry>
<entry>server private key</entry>
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 <openssl/ec.h>
#endif
#include <openssl/x509v3.h>
+#include <openssl/ocsp.h>
/* 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 <openssl/engine.h>
#endif
#include <openssl/x509v3.h>
+#include <openssl/ocsp.h>
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
Hi Hackers,
This is the third version patch for "Certificate status check using OCSP
Stapling" with ssl regression test cases added.
Here is how I run the ssl regression test:
./configure --enable-tap-tests --with-openssl
make -j
cd src/test/ssl
make sslfiles
make check PG_TEST_EXTRA=ssl
expected results:
# +++ tap check in src/test/ssl +++
t/001_ssltests.pl .. ok
t/002_scram.pl ..... ok
t/003_sslinfo.pl ... ok
All tests successful.
Files=3, Tests=279, 17 wallclock secs ( 0.05 usr 0.01 sys + 2.32
cusr 2.16 csys = 4.54 CPU)
Result: PASS
Notes, before executing the SSL regression tests with the command `make
check PG_TEST_EXTRA=ssl`, it is necessary to wait for 1 minute after
running `make sslfiles`. This delay is required because the newly
generated OCSP responses for the 'expired' test cases need 1 minute to
pass the nextUpdate period. Once the stapled OCSP response files for the
tests are committed as test input, there is no need to wait, similar to
certificate files.
Any comments or feedback would be greatly appreciated!
Thank you,
David
Attachments:
0002-doc-support-certificate-status-check-using-OCSP-stap.patchtext/plain; charset=UTF-8; name=0002-doc-support-certificate-status-check-using-OCSP-stap.patchDownload
From de75ecb6eb6d19177e213c77da000766171c95a5 Mon Sep 17 00:00:00 2001
From: David Zhang <idrawone@gmail.com>
Date: Tue, 5 Mar 2024 15:32:05 -0800
Subject: [PATCH 2/3] doc - 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 ++++++
3 files changed, 53 insertions(+)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b38cbd714a..28c0d3e00a 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1262,6 +1262,23 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-ssl-ocsp-file" xreflabel="ssl_ocsp_file">
+ <term><varname>ssl_ocsp_file</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>ssl_ocsp_file</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ 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
+ <filename>postgresql.conf</filename> file or on the server command line.
+ The default is empty, meaning no OCSP Stapling file is loaded.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-ssl-crl-file" xreflabel="ssl_crl_file">
<term><varname>ssl_crl_file</varname> (<type>string</type>)
<indexterm>
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
</listitem>
</varlistentry>
+ <varlistentry id="libpq-connect-sslocspstapling" xreflabel="sslocspstapling">
+ <term><literal>sslocspstapling</literal></term>
+ <listitem>
+ <para>
+ If set to 1, an <acronym>SSL</acronym> connection to the server
+ will send a certificate status request. <application>libpq</application>
+ will then refuse to connect if the server does not provide a valid OCSP
+ Stapling response. If set to 0 (default),
+ <application>libpq</application> will neither request the certificate
+ status nor check OCSP Stapling response. This option is only available if
+ <productname>PostgreSQL</productname> is compiled with SSL support.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-connect-sslkey" xreflabel="sslkey">
<term><literal>sslkey</literal></term>
<listitem>
@@ -8323,6 +8338,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
</para>
</listitem>
+ <listitem>
+ <para>
+ <indexterm>
+ <primary><envar>PGSSLOCSPSTAPLING</envar></primary>
+ </indexterm>
+ <envar>PGSSLOCSPSTAPLING</envar> behaves the same as the <xref
+ linkend="libpq-connect-sslocspstapling"/> connection parameter.
+ </para>
+ </listitem>
+
<listitem>
<para>
<indexterm>
@@ -8899,6 +8924,11 @@ ldap://ldap.acme.com/cn=dbserver,cn=hosts?pgconnectinfo?base?(objectclass=*)
Windows).
</para>
+ <para>
+ Stapled server certificate status (OCSP Stapling) will be checked,
+ if the parameter <literal>sslocspstapling</literal> is set to 1.
+ </para>
+
<para>
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 6047b8171d..0fa946a2ee 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2405,6 +2405,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
<entry>sent to client to indicate server's identity</entry>
</row>
+ <row>
+ <entry><xref linkend="guc-ssl-ocsp-file"/></entry>
+ <entry>server certificate status stapled by ocsp responder</entry>
+ <entry>sent to client to indicate server certificate status</entry>
+ </row>
+
<row>
<entry><xref linkend="guc-ssl-key-file"/> (<filename>$PGDATA/server.key</filename>)</entry>
<entry>server private key</entry>
--
2.34.1
0003-test-support-certificate-status-check-using-OCSP-sta.patchtext/plain; charset=UTF-8; name=0003-test-support-certificate-status-check-using-OCSP-sta.patchDownload
From 442c78dc326d68af3e0b7453a2ec629d70196531 Mon Sep 17 00:00:00 2001
From: David Zhang <idrawone@gmail.com>
Date: Tue, 5 Mar 2024 15:38:35 -0800
Subject: [PATCH 3/3] test - support certificate status check using OCSP
staplin
---
src/test/ssl/conf/ocsp_ca.config | 18 +++
src/test/ssl/sslfiles.mk | 124 +++++++++++++++-
src/test/ssl/t/001_ssltests.pl | 198 ++++++++++++++++++++++++++
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 3 +
src/test/ssl/t/SSL/Server.pm | 4 +
5 files changed, 342 insertions(+), 5 deletions(-)
create mode 100644 src/test/ssl/conf/ocsp_ca.config
diff --git a/src/test/ssl/conf/ocsp_ca.config b/src/test/ssl/conf/ocsp_ca.config
new file mode 100644
index 0000000000..b866085911
--- /dev/null
+++ b/src/test/ssl/conf/ocsp_ca.config
@@ -0,0 +1,18 @@
+# An OpenSSL format CSR config file for creating the ocsp responder certificate.
+# This configuration file is also used when operating the CA.
+#
+# This certificate is used to sign OCSP resonpose.
+
+[ req ]
+distinguished_name = req_distinguished_name
+prompt = no
+req_extensions = v3_ocsp
+
+[ req_distinguished_name ]
+CN = Test CA for PostgreSQL SSL regression test ocsp response
+
+# Extensions for OCSP responder certs
+[ v3_ocsp ]
+basicConstraints = CA:false
+keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+extendedKeyUsage = OCSPSigning
diff --git a/src/test/ssl/sslfiles.mk b/src/test/ssl/sslfiles.mk
index 88c93ec18d..fd92970698 100644
--- a/src/test/ssl/sslfiles.mk
+++ b/src/test/ssl/sslfiles.mk
@@ -35,6 +35,14 @@ SERVERS := server-cn-and-alt-names \
server-revoked
CLIENTS := client client-dn client-revoked client_ext client-long \
client-revoked-utf8
+OCSPS := server-ocsp-good \
+ server-ocsp-revoked \
+ server-ocsp-expired \
+ server-ocsp-unknown \
+ server-ca-ocsp-good \
+ server-ca-ocsp-revoked \
+ server-ca-ocsp-expired \
+ server-ca-ocsp-unknown
#
# To add a new non-standard certificate, add it to SPECIAL_CERTS and then add
@@ -62,14 +70,16 @@ COMBINATIONS := \
ssl/root+client_ca.crt \
ssl/root+client.crl \
ssl/client+client_ca.crt \
- ssl/server-cn-only+server_ca.crt
+ ssl/server-cn-only+server_ca.crt \
+ ssl/server-ip-cn-only+server_ca.crt
-CERTIFICATES := root_ca server_ca client_ca $(SERVERS) $(CLIENTS)
+CERTIFICATES := root_ca server_ca client_ca ocsp_ca $(SERVERS) $(CLIENTS)
STANDARD_CERTS := $(CERTIFICATES:%=ssl/%.crt)
STANDARD_KEYS := $(CERTIFICATES:%=ssl/%.key)
CRLS := ssl/root.crl \
ssl/client.crl \
ssl/server.crl
+OCSPRES := $(OCSPS:%=ssl/%.res)
SSLFILES := \
$(STANDARD_CERTS) \
@@ -77,7 +87,8 @@ SSLFILES := \
$(SPECIAL_CERTS) \
$(SPECIAL_KEYS) \
$(COMBINATIONS) \
- $(CRLS)
+ $(CRLS) \
+ $(OCSPRES)
SSLDIRS := ssl/client-crldir \
ssl/server-crldir \
ssl/root+client-crldir \
@@ -154,6 +165,9 @@ ssl/client+client_ca.crt: ssl/client.crt ssl/client_ca.crt
# for the server, to present to a client that only knows the root
ssl/server-cn-only+server_ca.crt: ssl/server-cn-only.crt ssl/server_ca.crt
+# for the server, to check when stapled ocsp response doesn't match server certificate
+ssl/server-ip-cn-only+server_ca.crt: ssl/server-ip-cn-only.crt ssl/server_ca.crt
+
# If a CRL is used, OpenSSL requires a CRL file for *all* the CAs in the
# chain, even if some of them are empty.
ssl/root+server.crl: ssl/root.crl ssl/server.crl
@@ -174,7 +188,7 @@ $(STANDARD_KEYS):
# Standard certificates
#
-CA_CERTS := ssl/server_ca.crt ssl/client_ca.crt
+CA_CERTS := ssl/server_ca.crt ssl/client_ca.crt ssl/ocsp_ca.crt
SERVER_CERTS := $(SERVERS:%=ssl/%.crt)
CLIENT_CERTS := $(CLIENTS:%=ssl/%.crt)
@@ -227,6 +241,106 @@ ssl/%-certindex.attr:
ssl/%.srl:
date +%Y%m%d%H%M%S00 > $@
+#
+# OCSP
+#
+.INTERMEDIATE: $(OCSPS:%=ssl/%.idx)
+# given status 'V' without 'revocation date' to generate an ocsp response with status 'good' for 10000 days
+ssl/server-ocsp-good.idx: ssl/server-cn-only.crt
+ expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
+ serial_number=$$($(OPENSSL) x509 -in $< -noout -serial | cut -d "=" -f 2); \
+ cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
+ echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" > $@
+
+# given status 'R' and 'revocation date' to generate an ocsp response with status 'revoked' for 10000 days
+ssl/server-ocsp-revoked.idx: ssl/server-cn-only.crt
+ expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
+ serial_number=$$($(OPENSSL) x509 -in $< -noout -serial | cut -d "=" -f 2); \
+ revocation_date=$$(date --utc +'%y%m%d%H%M%SZ'); \
+ cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
+ echo "R\t$$expiration_date\t$$revocation_date\t$$serial_number\tunknown\t$$cert_subject" > $@
+
+# generate an ocsp response with status 'unknown' using a none-existing certificate serial number 1970010100000000
+ssl/server-ocsp-unknown.idx: ssl/server-cn-only.crt
+ expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
+ serial_number="1970010100000000"; \
+ cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
+ echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" > $@
+
+# generate an ocsp response with status 'good' but nextUpdate 'expired' in only 1 minute
+ssl/server-ocsp-expired.idx: ssl/server-cn-only.crt
+ expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
+ serial_number=$$($(OPENSSL) x509 -in $< -noout -serial | cut -d "=" -f 2); \
+ cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
+ echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" > $@
+
+# server-cn-only.crt (good), ocsp response for server_ca.crt in (good|revoked|unknown|expired)
+# good, good
+ssl/server-ca-ocsp-good.idx: ssl/server_ca.crt ssl/server-ocsp-good.idx
+ expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
+ serial_number=$$($(OPENSSL) x509 -in $< -noout -serial | cut -d "=" -f 2); \
+ cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
+ cat ssl/server-ocsp-good.idx > $@; \
+ echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" >> $@
+
+# good, revoked
+ssl/server-ca-ocsp-revoked.idx: ssl/server_ca.crt ssl/server-ocsp-good.idx
+ expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
+ serial_number=$$($(OPENSSL) x509 -in $< -noout -serial | cut -d "=" -f 2); \
+ revocation_date=$$(date --utc +'%y%m%d%H%M%SZ'); \
+ cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
+ cat ssl/server-ocsp-good.idx > $@; \
+ echo "R\t$$expiration_date\t$$revocation_date\t$$serial_number\tunknown\t$$cert_subject" >> $@
+
+# good, unknown
+ssl/server-ca-ocsp-unknown.idx: ssl/server_ca.crt ssl/server-ocsp-good.idx
+ expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
+ serial_number="1970010100000001"; \
+ cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
+ cat ssl/server-ocsp-good.idx > $@; \
+ echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" >> $@
+
+# good, expired
+ssl/server-ca-ocsp-expired.idx: ssl/server_ca.crt ssl/server-ocsp-good.idx
+ expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
+ serial_number=$$($(OPENSSL) x509 -in $< -noout -serial | cut -d "=" -f 2); \
+ cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
+ cat ssl/server-ocsp-good.idx > $@; \
+ echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" >> $@
+
+$(OCSPRES):
+# server-cn-only: 'good'
+ssl/server-ocsp-good.res: ssl/server-ocsp-good.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt
+ $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -issuer ssl/server_ca.crt -ndays 10000 -cert ssl/server-cn-only.crt -respout $@
+
+# server-cn-only: 'revoked'
+ssl/server-ocsp-revoked.res: ssl/server-ocsp-revoked.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt
+ $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -issuer ssl/server_ca.crt -ndays 10000 -cert ssl/server-cn-only.crt -respout $@
+
+# server-cn-only: 'unknown'
+ssl/server-ocsp-unknown.res: ssl/server-ocsp-unknown.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt
+ $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -issuer ssl/server_ca.crt -ndays 10000 -cert ssl/server-cn-only.crt -respout $@
+
+# server-cn-only: 'expired'
+ssl/server-ocsp-expired.res: ssl/server-ocsp-expired.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt
+ $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -issuer ssl/server_ca.crt -nmin 1 -cert ssl/server-cn-only.crt -respout $@
+
+# server-cn-only, server_ca: 'good, good'
+ssl/server-ca-ocsp-good.res: ssl/server-ca-ocsp-good.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt ssl/server_ca.crt
+ $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -ndays 10000 -issuer ssl/server_ca.crt -cert ssl/server-cn-only.crt -issuer ssl/root_ca.crt -cert ssl/server_ca.crt -respout $@
+
+# server-cn-only, server_ca: 'good, revoked'
+ssl/server-ca-ocsp-revoked.res: ssl/server-ca-ocsp-revoked.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt ssl/server_ca.crt
+ $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -ndays 10000 -issuer ssl/server_ca.crt -cert ssl/server-cn-only.crt -issuer ssl/root_ca.crt -cert ssl/server_ca.crt -respout $@
+
+# server-cn-only, server_ca: 'good, unknown'
+ssl/server-ca-ocsp-unknown.res: ssl/server-ca-ocsp-unknown.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt ssl/server_ca.crt
+ $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -ndays 10000 -issuer ssl/server_ca.crt -cert ssl/server-cn-only.crt -issuer ssl/root_ca.crt -cert ssl/server_ca.crt -respout $@
+
+# server-cn-only, server_ca: 'good, expired'
+ssl/server-ca-ocsp-expired.res: ssl/server-ca-ocsp-expired.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt ssl/server_ca.crt
+ $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -nmin 1 -issuer ssl/server_ca.crt -cert ssl/server-cn-only.crt -issuer ssl/root_ca.crt -cert ssl/server_ca.crt -respout $@
+
#
# CRLs
#
@@ -262,7 +376,7 @@ ssl/%-crldir:
.PHONY: sslfiles-clean
sslfiles-clean:
- rm -f $(SSLFILES) ssl/*.old ssl/*.csr ssl/*.srl ssl/*-certindex*
+ rm -f $(SSLFILES) ssl/*.old ssl/*.csr ssl/*.srl ssl/*-certindex* ssl/*.idx ssl/*.res
rm -rf $(SSLDIRS) ssl/new_certs_dir
# The difference between the below clean targets and sslfiles-clean is that the
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 94ff043c8e..92656542ae 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -904,4 +904,202 @@ $node->connect_fails(
# ]
);
+# Test ocsp stapling.
+# Use a stapled ocsp response with different status for certificate server-cn-only
+# so that the client can verify the ocsp response when sslocspstapling is set.
+# server-cn-only certificates status is 'good'
+switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ keyfile => 'server-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ocsp-good');
+
+# Reset the common_connstr
+$common_connstr =
+ "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=ocsp-good.pg-ssltest.test";
+
+# Continue the TLS connection when certificate status is 'good' in stapled ocsp response.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "connect with valid stapled ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+# server-cn-only certificates status is 'revoked'
+switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ keyfile => 'server-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ocsp-revoked');
+
+# Fail the TLS connection when certificate status is 'revoked' in stapled ocsp response.
+$node->connect_fails(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "failed with a revoked ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+# server-cn-only certificates status is 'unknown'
+switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ keyfile => 'server-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ocsp-unknown');
+
+# Fail the TLS connection when certificate status is 'unknown' in stapled ocsp response.
+$node->connect_fails(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "failed with a revoked ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+# server-cn-only certificates status is 'expired'
+switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ keyfile => 'server-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ocsp-expired');
+
+# Fail the TLS connection when stapled ocsp response is 'expired'.
+$node->connect_fails(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "failed with an expired ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+# Use a stapled ocsp response with different status for certificate server-cn-only and server_ca
+# so that the client can verify the ocsp response when sslocspstapling is set.
+# server-cn-only, server_ca: 'good, good'
+switch_server_cert(
+ $node,
+ certfile => 'server-cn-only+server_ca',
+ keyfile => 'server-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ca-ocsp-good');
+
+# Reset the common_connstr
+$common_connstr =
+ "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=ocsp-good.pg-ssltest.test";
+
+# Continue the TLS connection when certificate status is 'good' in stapled ocsp response.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "connect with valid stapled ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+# server-cn-only, server_ca: 'good, revoked'
+switch_server_cert(
+ $node,
+ certfile => 'server-cn-only+server_ca',
+ keyfile => 'server-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ca-ocsp-revoked');
+
+# Fail the TLS connection when certificate status is 'revoked' in stapled ocsp response.
+$node->connect_fails(
+ "$common_connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "failed with a revoked ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+# server-cn-only, server_ca: 'good, unknown'
+switch_server_cert(
+ $node,
+ certfile => 'server-cn-only+server_ca',
+ keyfile => 'server-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ca-ocsp-unknown');
+
+# Fail the TLS connection when certificate status is 'unknown' in stapled ocsp response.
+$node->connect_fails(
+ "$common_connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "failed with a revoked ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+# server-cn-only, server_ca: 'good, expired'
+switch_server_cert(
+ $node,
+ certfile => 'server-cn-only+server_ca',
+ keyfile => 'server-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ca-ocsp-expired');
+
+# Fail the TLS connection when stapled ocsp response is 'expired'.
+$node->connect_fails(
+ "$common_connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "failed with an expired ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+
+# Use a stapled ocsp response which doesn't match the certificate server-ip-cn-only
+# so that the client will fail the TLS connection when sslocspstapling is set.
+switch_server_cert(
+ $node,
+ certfile => 'server-ip-cn-only',
+ keyfile => 'server-ip-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ocsp-good');
+
+# Fail the TLS connection when stapled ocsp response doesn't match certificate.
+$node->connect_fails(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "failed with an expired ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+
+# Use a stapled ocsp response which doesn't match the certificate server-ip-cn-only
+# so that the client will fail the TLS connection when sslocspstapling is set.
+switch_server_cert(
+ $node,
+ certfile => 'server-ip-cn-only+server_ca',
+ keyfile => 'server-ip-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ca-ocsp-good');
+
+# Fail the TLS connection when stapled ocsp response doesn't match certificate.
+$node->connect_fails(
+ "$common_connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "failed with an expired ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+
done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 410b4b1a3f..3f4c1a0fe4 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -73,6 +73,7 @@ sub init
_copy_files("ssl/root+client_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
+ _copy_files("ssl/server-*.res", $pgdata);
mkdir("$pgdata/root+client-crldir")
or die "unable to create server CRL dir $pgdata/root+client-crldir: $!";
_copy_files("ssl/root+client-crldir/*", "$pgdata/root+client-crldir/");
@@ -186,6 +187,8 @@ sub set_server_cert
. "ssl_crl_file='$params->{crlfile}'\n";
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
+ $sslconf .= "ssl_ocsp_file='$params->{ocspfile}.res'\n"
+ if defined $params->{ocspfile};
return $sslconf;
}
diff --git a/src/test/ssl/t/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
index 149a938511..f806b433da 100644
--- a/src/test/ssl/t/SSL/Server.pm
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -255,6 +255,10 @@ The CA certificate to use. Implementation is SSL backend specific.
The certificate file to use. Implementation is SSL backend specific.
+=item ocspfile => B<value>
+
+The ocsp stapling file to use. Implementation is SSL backend specific.
+
=item keyfile => B<value>
The private key file to use. Implementation is SSL backend specific.
--
2.34.1
0001-support-certificate-status-check-using-OCSP-stapling.patchtext/plain; charset=UTF-8; name=0001-support-certificate-status-check-using-OCSP-stapling.patchDownload
From 99fc46ed0bf05eedbe7539890d946db472617150 Mon Sep 17 00:00:00 2001
From: David Zhang <idrawone@gmail.com>
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 <openssl/ec.h>
#endif
#include <openssl/x509v3.h>
+#include <openssl/ocsp.h>
/* 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 <openssl/engine.h>
#endif
#include <openssl/x509v3.h>
+#include <openssl/ocsp.h>
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
On Tue, Mar 5, 2024 at 4:12 PM David Zhang <david.zhang@highgo.ca> wrote:
Any comments or feedback would be greatly appreciated!
Hi David -- I haven't had time to get to this for the 17 release
cycle, but I'm interested in this feature and I intend to review it at
some point for 18. I think OCSP will be especially helpful for anyone
relying on sslrootcert=system. Thanks for working on it!
--Jacob
On Tue, Mar 5, 2024 at 4:12 PM David Zhang <david.zhang@highgo.ca> wrote:
This is the third version patch for "Certificate status check using OCSP
Stapling" with ssl regression test cases added.
Hi David,
Thanks again for working on this! So far I've taken a look at the
design and tests. I've only skimmed the callback implementations; I
plan to review those in more detail once the architecture is nailed
down.
= Design =
It looks like this design relies on the DBA to manually prefetch OCSP
responses for their cert chain, and cache them in the local
ssl_ocsp_file. This is similar to Nginx's ssl_stapling_file directive
[1]: https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_stapling_file
going to take a lot of good documentation on what, exactly, an admin
has to do to make sure their response file is fresh. IIUC it will
depend on the CA's policy and how they operate their responder.
We should probably be prepared for complaints that we don't run the
client ourselves. A v2 could maybe include an extension for running
the OCSP client in a background worker? Or maybe that's overkill, and
an existing job scheduler extension could do it, but having a custom
worker that could periodically check the response file and provide
warnings when it gets close to expiring might be helpful for admins.
I am worried about the multi-stapling that is being done in the tests,
for example the server-ca-ocsp-good response file. I think those tests
are cheating a bit. Yes, for this particular case, we can issue a
single signed response for both the intermediate and the leaf -- but
that's only because we issued both of them. What if your intermediate
and your root use different responders [2]as appears to be the case for postgresql.org? What if your issuer's
responder doesn't even support multiple certs per request [3]https://community.letsencrypt.org/t/bulk-ocsp-requests/168156/2?
(Note that OpenSSL still doesn't support multi-stapling [4]https://github.com/openssl/openssl/pull/20945, even in
TLS 1.3 where we were supposed to get it "for free".)
I think it would be good for the sslocspstapling directive to 1) maybe
have a shorter name (cue bikeshed!) and 2) support more than a binary
on/off. For example, the current implementation could use
"disable/require" options, and then a v2 could add "prefer" which
simply sends the status request and honors must-staple extensions on
the certificate. (That should be safe to default to, I think, and it
would let DBAs control the stapling rollout more easily.)
A question from ignorance: how does the client decide that the
signature on the OCSP response is actually valid for the specific
chain in use?
= Tests =
I think the tests should record the expected_stderr messages for
failures (see the Code section below for why).
If it turns out that multi-stapling is safe, then IMO the tests should
explicitly test both TLSv1.2 and v1.3, since those protocols differ in
how they handle per-certificate status.
If it's okay with you, I'd like to volunteer to refactor out the
duplicated recipes in sslfiles.mk. I have some particular ideas in
mind and don't want to make you play fetch-a-rock. (No pressure to use
what I come up with, if you don't like it.)
Because a CRL is a valid fallback for OCSP in practice, I think there
should be some tests for their interaction. (For v1, maybe that's as
simple as "you're not allowed to use both yet", but it should be
explicit.)
= Code =
(this is a very shallow review)
+#define OCSP_CERT_STATUS_OK 1
+#define OCSP_CERT_STATUS_NOK (-1)
Returning f -1 from the callback indicates an internal error, so we're
currently sending the wrong alerts for OCSP failures ("internal error"
rather than "bad certificate status response") and getting unhelpful
error messages from libpq. For cases where the handshake proceeds
correctly but we don't accept the OCSP response status, I think we
should be returning zero.
Also, we should not stomp on the OCSP_ namespace (I thought these
macros were part of the official <openssl/ocsp.h> API at first).
+ /* set up OCSP stapling callback */
+ SSL_CTX_set_tlsext_status_cb(SSL_context, ocsp_stapling_cb);
It seems like this should only be done if ssl_ocsp_file is set?
Thanks again!
--Jacob
[1]: https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_stapling_file
[2]: as appears to be the case for postgresql.org
[3]: https://community.letsencrypt.org/t/bulk-ocsp-requests/168156/2
[4]: https://github.com/openssl/openssl/pull/20945
= Design =
It looks like this design relies on the DBA to manually prefetch OCSP
responses for their cert chain, and cache them in the local
ssl_ocsp_file. This is similar to Nginx's ssl_stapling_file directive
[1]. I think this may make sense for a v1 (much less code!), but it's
going to take a lot of good documentation on what, exactly, an admin
has to do to make sure their response file is fresh. IIUC it will
depend on the CA's policy and how they operate their responder.We should probably be prepared for complaints that we don't run the
client ourselves. A v2 could maybe include an extension for running
the OCSP client in a background worker? Or maybe that's overkill, and
an existing job scheduler extension could do it, but having a custom
worker that could periodically check the response file and provide
warnings when it gets close to expiring might be helpful for admins.
Totally agree. Either Implementing OCSP requests over HTTP, then parsing
the response and then saving the results to a file, or using an OpenSSL
client with a cron job to periodically update the file should work.
Using a cron job would likely have less impact on PostgreSQL.
I am worried about the multi-stapling that is being done in the tests,
for example the server-ca-ocsp-good response file. I think those tests
are cheating a bit. Yes, for this particular case, we can issue a
single signed response for both the intermediate and the leaf -- but
that's only because we issued both of them. What if your intermediate
and your root use different responders [2]? What if your issuer's
responder doesn't even support multiple certs per request [3]?(Note that OpenSSL still doesn't support multi-stapling [4], even in
TLS 1.3 where we were supposed to get it "for free".)
Totally agree, then we should limit OCSP stapling check for the
leaf/PostgreSQL server certificate only in v1.
I think it would be good for the sslocspstapling directive to 1) maybe
have a shorter name (cue bikeshed!) and 2) support more than a binary
on/off. For example, the current implementation could use
"disable/require" options, and then a v2 could add "prefer" which
simply sends the status request and honors must-staple extensions on
the certificate. (That should be safe to default to, I think, and it
would let DBAs control the stapling rollout more easily.)
This will definitely give end users more options, especially during the
transition period.
A question from ignorance: how does the client decide that the
signature on the OCSP response is actually valid for the specific
chain in use?
If I understand correctly, the certificate used by the OCSP responder to
sign the OCSP response must be valid for the specific chain in use, or
the admins allow to load a new chain to validate the certificate used to
sign the OCSP response. I think it would be better to make this part to
be more open.
= Tests =
I think the tests should record the expected_stderr messages for
failures (see the Code section below for why).
+1
If it turns out that multi-stapling is safe, then IMO the tests should
explicitly test both TLSv1.2 and v1.3, since those protocols differ in
how they handle per-certificate status.If it's okay with you, I'd like to volunteer to refactor out the
duplicated recipes in sslfiles.mk. I have some particular ideas in
mind and don't want to make you play fetch-a-rock. (No pressure to use
what I come up with, if you don't like it.)
That would be great, thanks a lot in advance!
Because a CRL is a valid fallback for OCSP in practice, I think there
should be some tests for their interaction. (For v1, maybe that's as
simple as "you're not allowed to use both yet", but it should be
explicit.)
+1
= Code =
(this is a very shallow review)
+#define OCSP_CERT_STATUS_OK 1
+#define OCSP_CERT_STATUS_NOK (-1)Returning f -1 from the callback indicates an internal error, so we're
currently sending the wrong alerts for OCSP failures ("internal error"
rather than "bad certificate status response") and getting unhelpful
error messages from libpq. For cases where the handshake proceeds
correctly but we don't accept the OCSP response status, I think we
should be returning zero.
+1
Also, we should not stomp on the OCSP_ namespace (I thought these
macros were part of the official <openssl/ocsp.h> API at first).
+1
+ /* set up OCSP stapling callback */ + SSL_CTX_set_tlsext_status_cb(SSL_context, ocsp_stapling_cb);It seems like this should only be done if ssl_ocsp_file is set?
+1
Thanks a lot for reviewing and providing all your feedback!
Best regards,
David Zhang
On Wed, Jul 17, 2024 at 3:42 PM David Zhang <idrawone@gmail.com> wrote:
Totally agree. Either Implementing OCSP requests over HTTP, then parsing
the response and then saving the results to a file, or using an OpenSSL
client with a cron job to periodically update the file should work.
Using a cron job would likely have less impact on PostgreSQL.
Yeah, my preference would be to farm this job out to OpenSSL entirely.
Implementing OCSP-over-HTTP ourselves seems unlikely to buy us much,
for all the work it would give us.
Totally agree, then we should limit OCSP stapling check for the
leaf/PostgreSQL server certificate only in v1.
Sounds good. Maybe a future version can implement a check of the full
chain, but I imagine we'll have to follow someone else's lead.
A question from ignorance: how does the client decide that the
signature on the OCSP response is actually valid for the specific
chain in use?If I understand correctly, the certificate used by the OCSP responder to
sign the OCSP response must be valid for the specific chain in use, or
the admins allow to load a new chain to validate the certificate used to
sign the OCSP response. I think it would be better to make this part to
be more open.
Okay. That part needs more design work IMO, and solid testing.
If it's okay with you, I'd like to volunteer to refactor out the
duplicated recipes in sslfiles.mk. I have some particular ideas in
mind and don't want to make you play fetch-a-rock. (No pressure to use
what I come up with, if you don't like it.)That would be great, thanks a lot in advance!
No problem! I've attached two patches that can be optionally applied
on top of yours:
- 0001 simplifies the response generation into a single recipe.
- 0002 is a bigger change that uses `openssl ca` to generate index
files, as opposed to constructing them manually ourselves.
(The makefile will get even smaller without multi-stapling support,
but I didn't want to combine that with the refactor.)
For 0002, I'm wiping the new CA index for each recipe and rebuilding
it from scratch, then copying it into place (this relies on the
.NOTPARALLEL setting up top for correctness). I think there's probably
an even simpler approach, which would be to generate a single index
that can produce all of our desired responses. I can give that a try
once multi-stapling support is pulled out.
Thanks a lot for reviewing and providing all your feedback!
You're very welcome, thanks for working on this feature!
--Jacob
Attachments:
0002-WIP-move-to-OpenSSL-constructed-index-files.patch.txttext/plain; charset=US-ASCII; name=0002-WIP-move-to-OpenSSL-constructed-index-files.patch.txtDownload
From 1f4f030233a895586aa7e114c92f4aa213250752 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Mon, 15 Jul 2024 06:40:07 -0700
Subject: [PATCH 2/2] WIP: move to OpenSSL-constructed index files
---
src/test/ssl/conf/ocsp.config | 19 +++++++++
src/test/ssl/sslfiles.mk | 75 ++++++++++++++---------------------
2 files changed, 48 insertions(+), 46 deletions(-)
create mode 100644 src/test/ssl/conf/ocsp.config
diff --git a/src/test/ssl/conf/ocsp.config b/src/test/ssl/conf/ocsp.config
new file mode 100644
index 0000000000..831c2f681c
--- /dev/null
+++ b/src/test/ssl/conf/ocsp.config
@@ -0,0 +1,19 @@
+[ ca ]
+default_ca = ocsp
+
+# A shell of a CA, mostly duplicating the server CA, which is used only during
+# the OCSP index generation recipes.
+[ ocsp ]
+dir = ./ssl/
+
+# The database (or "index") is the main thing we want.
+database = ./ssl/ocsp-certindex
+
+# Everything else should all be unused, so we specify whatever's most
+# convenient. In particular there's no need to have a unique cert/key pair for
+# this.
+certificate = ./ssl/server_ca.crt
+private_key = ./ssl/server_ca.key
+serial = ./ssl/ocsp_ca.srl
+default_md = sha256
+policy = policy_match
diff --git a/src/test/ssl/sslfiles.mk b/src/test/ssl/sslfiles.mk
index 9ba88f0be9..bfefa6a33e 100644
--- a/src/test/ssl/sslfiles.mk
+++ b/src/test/ssl/sslfiles.mk
@@ -196,6 +196,7 @@ CLIENT_CERTS := $(CLIENTS:%=ssl/%.crt)
root_ca_state_files := ssl/root_ca-certindex ssl/root_ca-certindex.attr ssl/root_ca.srl
server_ca_state_files := ssl/server_ca-certindex ssl/server_ca-certindex.attr ssl/server_ca.srl
client_ca_state_files := ssl/client_ca-certindex ssl/client_ca-certindex.attr ssl/client_ca.srl
+ocsp_ca_state_files := ssl/ocsp-certindex ssl/ocsp-certindex.attr ssl/ocsp_ca.srl
# These are the workhorse recipes. `openssl ca` can't be safely run from
# parallel processes, so we must mark the entire Makefile .NOTPARALLEL.
@@ -224,6 +225,7 @@ ssl/%.csr: ssl/%.key conf/%.config
#
.INTERMEDIATE: $(root_ca_state_files) $(server_ca_state_files) $(client_ca_state_files)
+.INTERMEDIATE: $(ocsp_ca_state_files)
# OpenSSL requires a directory to put all generated certificates in. We don't
# use this for anything, but we need a location.
@@ -245,68 +247,49 @@ ssl/%.srl:
# OCSP
#
.INTERMEDIATE: $(OCSPS:%=ssl/%.idx)
+
# given status 'V' without 'revocation date' to generate an ocsp response with status 'good' for 10000 days
-ssl/server-ocsp-good.idx: ssl/server-cn-only.crt
- expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
- serial_number=$$($(OPENSSL) x509 -in $< -noout -serial | cut -d "=" -f 2); \
- cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
- echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" > $@
+ssl/server-ocsp-good.idx: conf/ocsp.config ssl/server-cn-only.crt | $(ocsp_ca_state_files)
+ : > ssl/ocsp-certindex
+ openssl ca -config conf/ocsp.config -valid ssl/server-cn-only.crt
+ cp ssl/ocsp-certindex $@
# given status 'R' and 'revocation date' to generate an ocsp response with status 'revoked' for 10000 days
-ssl/server-ocsp-revoked.idx: ssl/server-cn-only.crt
- expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
- serial_number=$$($(OPENSSL) x509 -in $< -noout -serial | cut -d "=" -f 2); \
- revocation_date=$$(date --utc +'%y%m%d%H%M%SZ'); \
- cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
- echo "R\t$$expiration_date\t$$revocation_date\t$$serial_number\tunknown\t$$cert_subject" > $@
+ssl/server-ocsp-revoked.idx: conf/ocsp.config ssl/server-cn-only.crt | $(ocsp_ca_state_files)
+ : > ssl/ocsp-certindex
+ openssl ca -config conf/ocsp.config -revoke ssl/server-cn-only.crt
+ cp ssl/ocsp-certindex $@
# generate an ocsp response with status 'unknown' using a none-existing certificate serial number 1970010100000000
-ssl/server-ocsp-unknown.idx: ssl/server-cn-only.crt
- expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
- serial_number="1970010100000000"; \
- cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
- echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" > $@
+ssl/server-ocsp-unknown.idx:
+ touch $@
# generate an ocsp response with status 'good' but nextUpdate 'expired' in only 1 minute
-ssl/server-ocsp-expired.idx: ssl/server-cn-only.crt
- expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
- serial_number=$$($(OPENSSL) x509 -in $< -noout -serial | cut -d "=" -f 2); \
- cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
- echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" > $@
+ssl/server-ocsp-expired.idx: ssl/server-ocsp-good.idx
+ cp $< $@
# server-cn-only.crt (good), ocsp response for server_ca.crt in (good|revoked|unknown|expired)
# good, good
-ssl/server-ca-ocsp-good.idx: ssl/server_ca.crt ssl/server-ocsp-good.idx
- expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
- serial_number=$$($(OPENSSL) x509 -in $< -noout -serial | cut -d "=" -f 2); \
- cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
- cat ssl/server-ocsp-good.idx > $@; \
- echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" >> $@
+ssl/server-ca-ocsp-good.idx: conf/ocsp.config ssl/server-cn-only.crt ssl/server_ca.crt | $(ocsp_ca_state_files)
+ : > ssl/ocsp-certindex
+ openssl ca -config conf/ocsp.config -valid ssl/server-cn-only.crt
+ openssl ca -config conf/ocsp.config -valid ssl/server_ca.crt
+ cp ssl/ocsp-certindex $@
# good, revoked
-ssl/server-ca-ocsp-revoked.idx: ssl/server_ca.crt ssl/server-ocsp-good.idx
- expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
- serial_number=$$($(OPENSSL) x509 -in $< -noout -serial | cut -d "=" -f 2); \
- revocation_date=$$(date --utc +'%y%m%d%H%M%SZ'); \
- cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
- cat ssl/server-ocsp-good.idx > $@; \
- echo "R\t$$expiration_date\t$$revocation_date\t$$serial_number\tunknown\t$$cert_subject" >> $@
+ssl/server-ca-ocsp-revoked.idx: conf/ocsp.config ssl/server-cn-only.crt ssl/server_ca.crt | $(ocsp_ca_state_files)
+ : > ssl/ocsp-certindex
+ openssl ca -config conf/ocsp.config -valid ssl/server-cn-only.crt
+ openssl ca -config conf/ocsp.config -revoke ssl/server_ca.crt
+ cp ssl/ocsp-certindex $@
# good, unknown
-ssl/server-ca-ocsp-unknown.idx: ssl/server_ca.crt ssl/server-ocsp-good.idx
- expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
- serial_number="1970010100000001"; \
- cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
- cat ssl/server-ocsp-good.idx > $@; \
- echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" >> $@
+ssl/server-ca-ocsp-unknown.idx: ssl/server-ocsp-good.idx
+ cp $< $@
# good, expired
-ssl/server-ca-ocsp-expired.idx: ssl/server_ca.crt ssl/server-ocsp-good.idx
- expiration_date=$$($(OPENSSL) x509 -in $< -noout -dates | grep "notAfter" | cut -d "=" -f 2 | xargs -I {} date --date="{}" --utc +'%Y%m%d%H%M%S'Z); \
- serial_number=$$($(OPENSSL) x509 -in $< -noout -serial | cut -d "=" -f 2); \
- cert_subject=$$($(OPENSSL) x509 -in $< -noout -subject | sed 's/subject=OU = /\/OU=/' | sed 's/, CN = /\/CN=/'); \
- cat ssl/server-ocsp-good.idx > $@; \
- echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" >> $@
+ssl/server-ca-ocsp-expired.idx: ssl/server-ca-ocsp-good.idx
+ cp $< $@
# All of the responses have the server cert in the chain.
OCSPCHAIN = -issuer ssl/server_ca.crt -cert ssl/server-cn-only.crt
--
2.34.1
0001-WIP-simplify-.res-generation.patch.txttext/plain; charset=US-ASCII; name=0001-WIP-simplify-.res-generation.patch.txtDownload
From 7e7776ec8b3d09f2ad00c55897e2437d39b0f398 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 12 Jul 2024 11:40:18 -0700
Subject: [PATCH 1/2] WIP: simplify .res generation
---
src/test/ssl/sslfiles.mk | 49 ++++++++++++++--------------------------
1 file changed, 17 insertions(+), 32 deletions(-)
diff --git a/src/test/ssl/sslfiles.mk b/src/test/ssl/sslfiles.mk
index fd92970698..9ba88f0be9 100644
--- a/src/test/ssl/sslfiles.mk
+++ b/src/test/ssl/sslfiles.mk
@@ -308,38 +308,23 @@ ssl/server-ca-ocsp-expired.idx: ssl/server_ca.crt ssl/server-ocsp-good.idx
cat ssl/server-ocsp-good.idx > $@; \
echo "V\t$$expiration_date\t\t$$serial_number\tunknown\t$$cert_subject" >> $@
-$(OCSPRES):
-# server-cn-only: 'good'
-ssl/server-ocsp-good.res: ssl/server-ocsp-good.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt
- $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -issuer ssl/server_ca.crt -ndays 10000 -cert ssl/server-cn-only.crt -respout $@
-
-# server-cn-only: 'revoked'
-ssl/server-ocsp-revoked.res: ssl/server-ocsp-revoked.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt
- $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -issuer ssl/server_ca.crt -ndays 10000 -cert ssl/server-cn-only.crt -respout $@
-
-# server-cn-only: 'unknown'
-ssl/server-ocsp-unknown.res: ssl/server-ocsp-unknown.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt
- $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -issuer ssl/server_ca.crt -ndays 10000 -cert ssl/server-cn-only.crt -respout $@
-
-# server-cn-only: 'expired'
-ssl/server-ocsp-expired.res: ssl/server-ocsp-expired.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt
- $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -issuer ssl/server_ca.crt -nmin 1 -cert ssl/server-cn-only.crt -respout $@
-
-# server-cn-only, server_ca: 'good, good'
-ssl/server-ca-ocsp-good.res: ssl/server-ca-ocsp-good.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt ssl/server_ca.crt
- $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -ndays 10000 -issuer ssl/server_ca.crt -cert ssl/server-cn-only.crt -issuer ssl/root_ca.crt -cert ssl/server_ca.crt -respout $@
-
-# server-cn-only, server_ca: 'good, revoked'
-ssl/server-ca-ocsp-revoked.res: ssl/server-ca-ocsp-revoked.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt ssl/server_ca.crt
- $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -ndays 10000 -issuer ssl/server_ca.crt -cert ssl/server-cn-only.crt -issuer ssl/root_ca.crt -cert ssl/server_ca.crt -respout $@
-
-# server-cn-only, server_ca: 'good, unknown'
-ssl/server-ca-ocsp-unknown.res: ssl/server-ca-ocsp-unknown.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt ssl/server_ca.crt
- $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -ndays 10000 -issuer ssl/server_ca.crt -cert ssl/server-cn-only.crt -issuer ssl/root_ca.crt -cert ssl/server_ca.crt -respout $@
-
-# server-cn-only, server_ca: 'good, expired'
-ssl/server-ca-ocsp-expired.res: ssl/server-ca-ocsp-expired.idx ssl/server-cn-only.crt ssl/ocsp_ca.crt ssl/root+server_ca.crt ssl/server_ca.crt
- $(OPENSSL) ocsp -index $< -rsigner ssl/ocsp_ca.crt -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt -nmin 1 -issuer ssl/server_ca.crt -cert ssl/server-cn-only.crt -issuer ssl/root_ca.crt -cert ssl/server_ca.crt -respout $@
+# All of the responses have the server cert in the chain.
+OCSPCHAIN = -issuer ssl/server_ca.crt -cert ssl/server-cn-only.crt
+$(OCSPRES): ssl/server_ca.crt ssl/server-cn-only.crt
+
+# Additionally, the server CA is part of the server-ca-* responses.
+ssl/server-ca-%.res: OCSPCHAIN += -issuer ssl/root_ca.crt -cert ssl/server_ca.crt
+ssl/server-ca-%.res: ssl/root_ca.crt
+
+# Most responses should "never" expire, except the ones being explicitly tested
+# for expiration.
+OCSPEXP = -ndays 10000
+ssl/%-ocsp-expired.res: OCSPEXP = -nmin 1
+
+$(OCSPRES): ssl/%.res: ssl/%.idx ssl/ocsp_ca.crt ssl/ocsp_ca.key ssl/root+server_ca.crt
+ $(OPENSSL) ocsp -index $< -respout $@ -rsigner ssl/ocsp_ca.crt \
+ -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt \
+ $(OCSPEXP) $(OCSPCHAIN)
#
# CRLs
--
2.34.1
Thanks a lot Jacob for helping update the tests and sorry for the late
reply.
Based on previous discussion, I remove the document patch, and start to
focus on the v1 simple OCSP logic by checking the leaf/Postgres server
certificate's status only
(0001-v1-WIP-OCSP-support-certificate-status-check.patch). I also merge
your changes and simplify the test by testing the Postgres server
certificate's status only
(0002-v1-WIP-OCSP-simplify-.res-generation-and-regress-tes.patch).
On 2024-07-18 10:18 a.m., Jacob Champion wrote:
A question from ignorance: how does the client decide that the
signature on the OCSP response is actually valid for the specific
chain in use?If I understand correctly, the certificate used by the OCSP responder to
sign the OCSP response must be valid for the specific chain in use, or
the admins allow to load a new chain to validate the certificate used to
sign the OCSP response. I think it would be better to make this part to
be more open.Okay. That part needs more design work IMO, and solid testing.
Based on the RFC here,
https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.2.2. My
understanding is that the OCSP responder's certificate should be
directly signed by the same CA which signed the Postgres Server's
certificate. It looks the openssl 3.0.14 implements in this way. In
other words, it the OCSP responder's certificate is signed by a
different branch of the chain, then openssl will report some errors.
Unless the end user explicitly provides the OCSP responder's certificate
with trust_other option. In this case it will skip the some certificate
verification. I think it should be simple enough for v1 by set
`OCSP_basic_verify(basic_resp, NULL, trusted_store, 0)`. The key
function OCSP_basic_verify is documented at here,
https://docs.openssl.org/3.0/man3/OCSP_resp_find_status/
Thank you,
David
Attachments:
0002-v1-WIP-OCSP-simplify-.res-generation-and-regress-tes.patchtext/plain; charset=UTF-8; name=0002-v1-WIP-OCSP-simplify-.res-generation-and-regress-tes.patchDownload
From 368c77885d7925334e8dabcce655b6a82f0a9a8f Mon Sep 17 00:00:00 2001
From: David Zhang <idrawone@gmail.com>
Date: Tue, 6 Aug 2024 15:38:14 -0700
Subject: [PATCH 2/2] v1 WIP OCSP simplify .res generation and regress test
---
src/test/ssl/conf/ocsp_ca.config | 39 ++++++++++
src/test/ssl/sslfiles.mk | 59 +++++++++++++--
src/test/ssl/t/001_ssltests.pl | 100 ++++++++++++++++++++++++++
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 3 +
src/test/ssl/t/SSL/Server.pm | 4 ++
5 files changed, 201 insertions(+), 4 deletions(-)
create mode 100644 src/test/ssl/conf/ocsp_ca.config
diff --git a/src/test/ssl/conf/ocsp_ca.config b/src/test/ssl/conf/ocsp_ca.config
new file mode 100644
index 0000000000..04f78bacb8
--- /dev/null
+++ b/src/test/ssl/conf/ocsp_ca.config
@@ -0,0 +1,39 @@
+# An OpenSSL format CSR config file for creating the ocsp responder certificate.
+# This configuration file is also used when operating the CA.
+#
+# This certificate is used to sign OCSP resonpose.
+
+[ req ]
+distinguished_name = req_distinguished_name
+prompt = no
+req_extensions = v3_ocsp
+
+[ req_distinguished_name ]
+CN = Test CA for PostgreSQL SSL regression test ocsp response
+
+# Extensions for OCSP responder certs
+[ v3_ocsp ]
+basicConstraints = CA:false
+keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+extendedKeyUsage = OCSPSigning
+
+[ ca ]
+default_ca = ocsp
+
+# A shell of a CA, mostly duplicating the server CA, which is used only during
+# the OCSP index generation recipes.
+[ ocsp ]
+dir = ./ssl/
+
+# The database (or "index") is the main thing we want.
+database = ./ssl/ocsp-certindex
+
+# Everything else should all be unused, so we specify whatever's most
+# convenient. In particular there's no need to have a unique cert/key pair for
+# this.
+certificate = ./ssl/server_ca.crt
+private_key = ./ssl/server_ca.key
+serial = ./ssl/ocsp_ca.srl
+default_md = sha256
+policy = policy_match
+
diff --git a/src/test/ssl/sslfiles.mk b/src/test/ssl/sslfiles.mk
index 88c93ec18d..6f314b7153 100644
--- a/src/test/ssl/sslfiles.mk
+++ b/src/test/ssl/sslfiles.mk
@@ -35,6 +35,10 @@ SERVERS := server-cn-and-alt-names \
server-revoked
CLIENTS := client client-dn client-revoked client_ext client-long \
client-revoked-utf8
+OCSPS := server-ocsp-good \
+ server-ocsp-revoked \
+ server-ocsp-expired \
+ server-ocsp-unknown
#
# To add a new non-standard certificate, add it to SPECIAL_CERTS and then add
@@ -64,12 +68,13 @@ COMBINATIONS := \
ssl/client+client_ca.crt \
ssl/server-cn-only+server_ca.crt
-CERTIFICATES := root_ca server_ca client_ca $(SERVERS) $(CLIENTS)
+CERTIFICATES := root_ca server_ca client_ca ocsp_ca $(SERVERS) $(CLIENTS)
STANDARD_CERTS := $(CERTIFICATES:%=ssl/%.crt)
STANDARD_KEYS := $(CERTIFICATES:%=ssl/%.key)
CRLS := ssl/root.crl \
ssl/client.crl \
ssl/server.crl
+OCSPRES := $(OCSPS:%=ssl/%.res)
SSLFILES := \
$(STANDARD_CERTS) \
@@ -77,7 +82,8 @@ SSLFILES := \
$(SPECIAL_CERTS) \
$(SPECIAL_KEYS) \
$(COMBINATIONS) \
- $(CRLS)
+ $(CRLS) \
+ $(OCSPRES)
SSLDIRS := ssl/client-crldir \
ssl/server-crldir \
ssl/root+client-crldir \
@@ -175,13 +181,14 @@ $(STANDARD_KEYS):
#
CA_CERTS := ssl/server_ca.crt ssl/client_ca.crt
-SERVER_CERTS := $(SERVERS:%=ssl/%.crt)
+SERVER_CERTS := $(SERVERS:%=ssl/%.crt) ssl/ocsp_ca.crt
CLIENT_CERTS := $(CLIENTS:%=ssl/%.crt)
# See the "CA State" section below.
root_ca_state_files := ssl/root_ca-certindex ssl/root_ca-certindex.attr ssl/root_ca.srl
server_ca_state_files := ssl/server_ca-certindex ssl/server_ca-certindex.attr ssl/server_ca.srl
client_ca_state_files := ssl/client_ca-certindex ssl/client_ca-certindex.attr ssl/client_ca.srl
+ocsp_ca_state_files := ssl/ocsp-certindex ssl/ocsp-certindex.attr ssl/ocsp_ca.srl
# These are the workhorse recipes. `openssl ca` can't be safely run from
# parallel processes, so we must mark the entire Makefile .NOTPARALLEL.
@@ -210,6 +217,7 @@ ssl/%.csr: ssl/%.key conf/%.config
#
.INTERMEDIATE: $(root_ca_state_files) $(server_ca_state_files) $(client_ca_state_files)
+.INTERMEDIATE: $(ocsp_ca_state_files)
# OpenSSL requires a directory to put all generated certificates in. We don't
# use this for anything, but we need a location.
@@ -227,6 +235,49 @@ ssl/%-certindex.attr:
ssl/%.srl:
date +%Y%m%d%H%M%S00 > $@
+#
+# OCSP
+#
+.INTERMEDIATE: $(OCSPS:%=ssl/%.idx)
+
+# to generate an ocsp response with status 'good'
+ssl/server-ocsp-good.idx: conf/ocsp_ca.config ssl/server-cn-only.crt | $(ocsp_ca_state_files)
+ : > ssl/ocsp-certindex
+ openssl ca -config conf/ocsp_ca.config -valid ssl/server-cn-only.crt
+ cp ssl/ocsp-certindex $@
+
+# to generate an ocsp response with status 'revoked'
+ssl/server-ocsp-revoked.idx: conf/ocsp_ca.config ssl/server-cn-only.crt | $(ocsp_ca_state_files)
+ : > ssl/ocsp-certindex
+ openssl ca -config conf/ocsp_ca.config -revoke ssl/server-cn-only.crt
+ cp ssl/ocsp-certindex $@
+
+# to generate an ocsp response with status 'unknown'
+ssl/server-ocsp-unknown.idx:
+ touch $@
+
+# to generate an ocsp response with status 'good' but nextUpdate 'expired' in 1 minute
+ssl/server-ocsp-expired.idx: ssl/server-ocsp-good.idx
+ cp $< $@
+
+# All of the responses have the server cert in the chain.
+OCSPCHAIN = -issuer ssl/server_ca.crt -cert ssl/server-cn-only.crt
+$(OCSPRES): ssl/server_ca.crt ssl/server-cn-only.crt
+
+# Additionally, the server CA is part of the server-ca-* responses.
+ssl/server-ca-%.res: OCSPCHAIN += -issuer ssl/root_ca.crt -cert ssl/server_ca.crt
+ssl/server-ca-%.res: ssl/root_ca.crt
+
+# Most responses should "never" expire, except the ones being explicitly tested
+# for expiration.
+OCSPEXP = -ndays 10000
+ssl/%-ocsp-expired.res: OCSPEXP = -nmin 1
+
+$(OCSPRES): ssl/%.res: ssl/%.idx ssl/ocsp_ca.crt ssl/ocsp_ca.key ssl/root+server_ca.crt
+ $(OPENSSL) ocsp -index $< -respout $@ -rsigner ssl/ocsp_ca.crt \
+ -rkey ssl/ocsp_ca.key -CA ssl/root+server_ca.crt \
+ $(OCSPEXP) $(OCSPCHAIN)
+
#
# CRLs
#
@@ -262,7 +313,7 @@ ssl/%-crldir:
.PHONY: sslfiles-clean
sslfiles-clean:
- rm -f $(SSLFILES) ssl/*.old ssl/*.csr ssl/*.srl ssl/*-certindex*
+ rm -f $(SSLFILES) ssl/*.old ssl/*.csr ssl/*.srl ssl/*-certindex* ssl/*.idx ssl/*.res
rm -rf $(SSLDIRS) ssl/new_certs_dir
# The difference between the below clean targets and sslfiles-clean is that the
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b877327023..39eace0c7f 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -905,4 +905,104 @@ $node->connect_fails(
# ]
);
+# Test ocsp stapling.
+# Use a stapled ocsp response with different status for certificate server-cn-only
+# so that the client can verify the ocsp response when sslocspstapling is set.
+# server-cn-only certificates status is 'good'
+switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ keyfile => 'server-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ocsp-good');
+
+# Reset the common_connstr
+$common_connstr =
+ "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=ocsp-good.pg-ssltest.test";
+
+# Continue the TLS connection when certificate status is 'good' in stapled ocsp response.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "connect with valid stapled ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+# server-cn-only certificates status is 'revoked'
+switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ keyfile => 'server-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ocsp-revoked');
+
+# Fail the TLS connection when certificate status is 'revoked' in stapled ocsp response.
+$node->connect_fails(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "failed with a revoked ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+# server-cn-only certificates status is 'unknown'
+switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ keyfile => 'server-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ocsp-unknown');
+
+# Fail the TLS connection when certificate status is 'unknown' in stapled ocsp response.
+$node->connect_fails(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "failed with a revoked ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+# server-cn-only certificates status is 'expired'
+switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ keyfile => 'server-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ocsp-expired');
+
+# Fail the TLS connection when stapled ocsp response is 'expired'.
+$node->connect_fails(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "failed with an expired ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+
+# Use a stapled ocsp response which doesn't match the certificate server-ip-cn-only
+# so that the client will fail the TLS connection when sslocspstapling is set.
+switch_server_cert(
+ $node,
+ certfile => 'server-ip-cn-only',
+ keyfile => 'server-ip-cn-only',
+ cafile => 'root_ca',
+ ocspfile => 'server-ocsp-good');
+
+# Fail the TLS connection when stapled ocsp response doesn't match certificate.
+$node->connect_fails(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=1",
+ "failed with an expired ocsp response when sslocspstapling=1");
+
+# Continue the TLS connection when no ocsp response is required.
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslocspstapling=0",
+ "connect without requesting ocsp response when sslocspstapling=0");
+
+
done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 410b4b1a3f..3f4c1a0fe4 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -73,6 +73,7 @@ sub init
_copy_files("ssl/root+client_ca.crt", $pgdata);
_copy_files("ssl/root_ca.crt", $pgdata);
_copy_files("ssl/root+client.crl", $pgdata);
+ _copy_files("ssl/server-*.res", $pgdata);
mkdir("$pgdata/root+client-crldir")
or die "unable to create server CRL dir $pgdata/root+client-crldir: $!";
_copy_files("ssl/root+client-crldir/*", "$pgdata/root+client-crldir/");
@@ -186,6 +187,8 @@ sub set_server_cert
. "ssl_crl_file='$params->{crlfile}'\n";
$sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
if defined $params->{crldir};
+ $sslconf .= "ssl_ocsp_file='$params->{ocspfile}.res'\n"
+ if defined $params->{ocspfile};
return $sslconf;
}
diff --git a/src/test/ssl/t/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
index 021eec74ab..97d49236ec 100644
--- a/src/test/ssl/t/SSL/Server.pm
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -261,6 +261,10 @@ The CA certificate to use. Implementation is SSL backend specific.
The certificate file to use. Implementation is SSL backend specific.
+=item ocspfile => B<value>
+
+The ocsp stapling file to use. Implementation is SSL backend specific.
+
=item keyfile => B<value>
The private key file to use. Implementation is SSL backend specific.
--
2.34.1
0001-v1-WIP-OCSP-support-certificate-status-check.patchtext/plain; charset=UTF-8; name=0001-v1-WIP-OCSP-support-certificate-status-check.patchDownload
From 0fa54f2517c5958f054eff99a30ebd9b41a28bac Mon Sep 17 00:00:00 2001
From: David Zhang <idrawone@gmail.com>
Date: Tue, 6 Aug 2024 15:35:45 -0700
Subject: [PATCH 1/2] v1 WIP OCSP support certificate status check
---
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 | 153 ++++++++++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
8 files changed, 291 insertions(+)
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 7e056abd5a..efd242591c 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -50,6 +50,7 @@
#include <openssl/ec.h>
#endif
#include <openssl/x509v3.h>
+#include <openssl/ocsp.h>
/* default init hook can be overridden by a shared library */
@@ -87,6 +88,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;
@@ -453,6 +456,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);
@@ -1768,3 +1774,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 ef20ea755b..d80e52f26d 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -35,6 +35,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 c0a52cdcc3..4b499dd70b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -4616,6 +4616,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 9ec9f97e92..7454564bd7 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.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 142c98462e..67be66d711 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -88,6 +88,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 360d9a4547..35b9e49978 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -329,6 +329,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.
@@ -449,6 +453,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 */
@@ -1686,6 +1691,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
*/
@@ -4704,6 +4721,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);
@@ -7656,6 +7674,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 b6fffd7b9b..5a6fa404b7 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -62,6 +62,7 @@
#include <openssl/engine.h>
#endif
#include <openssl/x509v3.h>
+#include <openssl/ocsp.h>
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 */
@@ -1184,6 +1188,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) != 1)
+ {
+ 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
@@ -2146,3 +2180,122 @@ 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 the signature and issuer of OCSP response.
+ * 4. Valid the certificate status in OCSP response.
+ *
+ * Cleanup:
+ * - Free allocated memory for the OCSP response and basic response.
+ */
+static int ocsp_response_check_cb(SSL *ssl)
+{
+ long resp_len = 0;
+ const unsigned char *resp_data;
+ OCSP_RESPONSE *ocsp_resp = NULL;
+ OCSP_BASICRESP *basic_resp = NULL;
+ X509_STORE *trusted_store = NULL;
+ int ocsp_status = OCSP_CERT_STATUS_NOK;
+
+ OCSP_CERTID *cert_id;
+ X509 *peer_cert = NULL;
+ X509 *peer_cert_issuer = NULL;
+
+ int cert_status;
+ int rev_reason;
+ ASN1_GENERALIZEDTIME *rev_time;
+ ASN1_GENERALIZEDTIME *this_update;
+ ASN1_GENERALIZEDTIME *next_update;
+
+ /*
+ * step-1: retrieve OCSP response
+ */
+ /* check if requested a certificate status */
+ if (SSL_get_tlsext_status_type(ssl) != TLSEXT_STATUSTYPE_ocsp)
+ return OCSP_CERT_STATUS_OK; /* didn't request this OCSP status */
+
+ /* check if got a correct OCSP response */
+ resp_len = SSL_get_tlsext_status_ocsp_resp(ssl, &resp_data);
+ if (resp_data == NULL)
+ goto cleanup; /* no proper OCSP response found */
+
+ /* convert the OCSP response to internal format */
+ ocsp_resp = d2i_OCSP_RESPONSE(NULL, &resp_data, resp_len);
+ if (ocsp_resp == NULL)
+ goto cleanup; /* failed to convert this 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 */
+
+ /*
+ * step-3: verify OCSP response is proper signed by a trusted signer
+ */
+ /* get local trusted certificate store */
+ trusted_store = SSL_CTX_get_cert_store(SSL_get_SSL_CTX(ssl));
+ if (trusted_store == NULL)
+ goto cleanup;
+
+ /* verify ocsp response signature and the issuer */
+ if (OCSP_basic_verify(basic_resp, NULL, trusted_store, 0) != 1)
+ goto cleanup; /* basic verification failed */
+
+ /*
+ * step-4: valid the certificate status inside OCSP response
+ */
+ /* get certificate and issuer to construct OCSP status lookup id */
+ peer_cert = SSL_get0_peer_certificate(ssl);
+ peer_cert_issuer = sk_X509_value(SSL_get0_verified_chain(ssl), 1);
+
+ cert_id = OCSP_cert_to_id(NULL, peer_cert, peer_cert_issuer);
+ if (cert_id == NULL)
+ goto cleanup;
+
+ /* get certificate status information */
+ if (OCSP_resp_find_status(basic_resp, cert_id,
+ &cert_status, &rev_reason, &rev_time, &this_update, &next_update) != 1)
+ goto cleanup;
+
+ /* check whether or not the certificate has been revoked */
+ if (cert_status == V_OCSP_CERTSTATUS_GOOD)
+ {
+ if (OCSP_check_validity(this_update, next_update, 0, -1) == 1)
+ ocsp_status = OCSP_CERT_STATUS_OK;
+ else
+ goto cleanup;
+ }
+ else
+ goto cleanup;
+
+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 f36d76bf3f..b56433fed0 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -412,6 +412,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
While I have only skimmed the patch so far and need more review before I can
comment on it, I do have a question on the expected use of OCSP support in
postgres. With OCSP becoming optional [0]https://lists.cabforum.org/pipermail/servercert-wg/2023-September/003998.html, and big providers like Let's
Encrypt deprecating OCSP [1]https://letsencrypt.org/2024/07/23/replacing-ocsp-with-crls.html, is this mainly targeting organizations running
their own CA with in-house OCSP?
--
Daniel Gustafsson
[0]: https://lists.cabforum.org/pipermail/servercert-wg/2023-September/003998.html
[1]: https://letsencrypt.org/2024/07/23/replacing-ocsp-with-crls.html
Hi Daniel,
Thank you for all the information.
On 2024-08-07 12:20 a.m., Daniel Gustafsson wrote:
While I have only skimmed the patch so far and need more review before I can
comment on it, I do have a question on the expected use of OCSP support in
postgres. With OCSP becoming optional [0], and big providers like Let's
Encrypt deprecating OCSP [1], is this mainly targeting organizations running
their own CA with in-house OCSP?--
Daniel Gustafsson[0] https://lists.cabforum.org/pipermail/servercert-wg/2023-September/003998.html
[1] https://letsencrypt.org/2024/07/23/replacing-ocsp-with-crls.html
Regarding the privacy concert, the OCSP Stapling works in a very similar
way as CRL but provide a more "real-time" certificate status checking.
When the Client/psql needs to check the Certificate Status using OCSP
Stapling, it doesn't connect to any 3rd party server, such as CA or OCSP
Responder. It only requires the Server/Postgres to provide one extra
piece of information about the status of Server's certificate. OCSP
Stapling was designed to avoid the privacy concern and a single point of
failure issues.
When the Client/psql needs to check the certificate revocation status:
option 1 using CRL: CA generates the CRL and then upload it to somewhere
or distribute it to each Client/psql. I think Postgres only support the
preloaded CRL.
option 2 using OCSP Stapling: Postgres server retrieves the certificate
status periodically from OCSP responder, the Client/psql will do the
certificate status check during each handshake.
I think it is still necessary to provide the 2nd option/flexibility to
end users if they are allowed to check the Postgres server's certificate
revocation status through CRL.
Thank you,
David
On Wed, Aug 7, 2024 at 12:20 AM Daniel Gustafsson <daniel@yesql.se> wrote:
While I have only skimmed the patch so far and need more review before I can
comment on it, I do have a question on the expected use of OCSP support in
postgres. With OCSP becoming optional [0], and big providers like Let's
Encrypt deprecating OCSP [1], is this mainly targeting organizations running
their own CA with in-house OCSP?
That announcement took me by surprise (and, it looks like, a number of
other people [1, 2]). I get that OCSP is expensive and painful for
Let's Encrypt, based on previous outages and blog posts, but I also
figured that Must-Staple was basically the best you could do without
being a browser. It already seemed pretty clear that we shouldn't
build a client-side OCSP check. Throwing server-side stapling under
the bus with it was unexpected.
Some of the LE quotes on the matter are giving me cart-before-horse vibes:
But it is clear to me OCSP is an ineffective technical dead-end, and we are all better served by moving on to figure out what else we can do.
We may keep OCSP running for some time for certificates that have the must-staple extension, to help smooth the transition, but at this time we don’t have a plan for how to actually deprecate OCSP: just an intent, publicized to ensure we can all begin to plan for a future without it.
It's pretty frustrating to hear about a "transition" when there is
nothing to transition to.
I honestly wonder if they're going to end up walking some of this
back. The messaging reminds me of "that one project" that every
company seems to have, where it's expensive and buggy as heck, all the
maintainers want to see it deleted, and they unilaterally declare over
clients' objections that they will, only to find at the last second
that the cure is worse than the disease and then finally resign
themselves to supporting it. Tears are shed, bridges burned.
Anyways, I look forward to seeing how broken my crystal ball is this
time. The timing is awful for this patchset in particular.
--Jacob
[1]: https://community.letsencrypt.org/t/sunsetting-of-ocsp-in-favor-of-older-technology/222589
[2]: https://community.letsencrypt.org/t/what-will-happen-to-must-staple/222397
On 15 Aug 2024, at 00:42, Jacob Champion <jacob.champion@enterprisedb.com> wrote:
It's pretty frustrating to hear about a "transition" when there is
nothing to transition to.
I guess they prefer that orgs transition back to just using CRL's.
Anyways, I look forward to seeing how broken my crystal ball is this
time. The timing is awful for this patchset in particular.
It is indeed, if it ends up deprecated server-side among the big providers then
support for it risks being very hard to use. Not sure what is the best course
of action here is.
--
Daniel Gustafsson
On Mon, Sep 2, 2024 at 5:55 AM Daniel Gustafsson <daniel@yesql.se> wrote:
I guess they prefer that orgs transition back to just using CRL's.
From a practical perspective, I don't think anyone but browsers can do
that right now. Best I can tell, there's no CRLite client other than
Firefox, and Google's CRLSets look like a manual emergency system
rather than a general-purpose tool.
I don't think we could do it manually even if we wanted to (and we
don't want to, IMHO, for a whole host of reasons). As one specific
example, take the certificate for postgresql.org. There's no CRL
distribution point listed, and an LE blog post [1]https://letsencrypt.org/2022/09/07/new-life-for-crls.html from a couple years
back implies that they have no plans to make those available to us:
Although we will be producing CRLs which cover all certificates that we
issue, we will not be including those URLs in the CRL Distribution Point
extension of our certificates. [...] Our new CRL URLs will be disclosed
only in CCADB, so that the Apple and Mozilla root programs can consume
them without exposing them to potentially large download traffic from
the rest of the internet at large.
Frankly, it looks like they have no plan for non-browser clients. It's
feeling like one of those "Web" vs. "Internet" splits.
--Jacob
[1]: https://letsencrypt.org/2022/09/07/new-life-for-crls.html