From 91044ae4baeac2e501e34164a69bd5d9c4976d21 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Mon, 8 Apr 2024 04:24:51 +0300 Subject: [PATCH] Send ALPN in TLS handshake, require it in direct SSL connections libpq now always tries to send ALPN. With the traditional negotiated SSL connections, the server accepts the ALPN, and refuses the connection if it's not what we expect, but connecting without ALPN is still OK. With the new direct SSL connections, ALPN is mandatory. NOTE: This uses "TBD-pgsql" as the protocol ID. We must register a proper one with IANA before the release! Author: Greg Stark, Heikki Linnakangas Reviewed-by: Matthias van de Meent, Jacob Champion --- doc/src/sgml/libpq.sgml | 12 ++++ src/backend/libpq/be-secure-openssl.c | 77 ++++++++++++++++++++++++ src/backend/tcop/backend_startup.c | 8 +++ src/bin/psql/command.c | 7 ++- src/include/libpq/libpq-be.h | 1 + src/include/libpq/pqcomm.h | 19 ++++++ src/interfaces/libpq/fe-secure-openssl.c | 35 +++++++++++ 7 files changed, 157 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 0fb728e2b2..0306a76161 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -2944,6 +2944,18 @@ const char *PQsslAttribute(const PGconn *conn, const char *attribute_name); + + alpn + + + Application protocol selected by the TLS Application-Layer + Protocol Negotiation (ALPN) extension. The only protocol + supported by libpq is TBD-pgsql, so this is + mainly useful for checking whether the server supported ALPN or + not. Empty string if ALPN was not used. + + + diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c index 72e43af353..6e877c0173 100644 --- a/src/backend/libpq/be-secure-openssl.c +++ b/src/backend/libpq/be-secure-openssl.c @@ -67,6 +67,12 @@ static int ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdat static int dummy_ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata); static int verify_cb(int ok, X509_STORE_CTX *ctx); static void info_cb(const SSL *ssl, int type, int args); +static int alpn_cb(SSL *ssl, + const unsigned char **out, + unsigned char *outlen, + const unsigned char *in, + unsigned int inlen, + void *userdata); static bool initialize_dh(SSL_CTX *context, bool isServerStart); static bool initialize_ecdh(SSL_CTX *context, bool isServerStart); static const char *SSLerrmessage(unsigned long ecode); @@ -432,6 +438,9 @@ be_tls_open_server(Port *port) /* set up debugging/info callback */ SSL_CTX_set_info_callback(SSL_context, info_cb); + /* enable ALPN */ + SSL_CTX_set_alpn_select_cb(SSL_context, alpn_cb, port); + if (!(port->ssl = SSL_new(SSL_context))) { ereport(COMMERROR, @@ -571,6 +580,32 @@ aloop: return -1; } + /* Get the protocol selected by ALPN */ + port->alpn_used = false; + { + const unsigned char *selected; + unsigned int len; + + SSL_get0_alpn_selected(port->ssl, &selected, &len); + + /* If ALPN is used, check that we negotiated the expected protocol */ + if (selected != NULL) + { + if (len == strlen(PG_ALPN_PROTOCOL) && + memcmp(selected, PG_ALPN_PROTOCOL, strlen(PG_ALPN_PROTOCOL)) == 0) + { + port->alpn_used = true; + } + else + { + /* shouldn't happen */ + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("received SSL connection request with unexpected ALPN protocol"))); + } + } + } + /* Get client certificate, if available. */ port->peer = SSL_get_peer_certificate(port->ssl); @@ -1259,6 +1294,48 @@ info_cb(const SSL *ssl, int type, int args) } } +/* See pqcomm.h comments on OpenSSL implementation of ALPN (RFC 7301) */ +static const unsigned char alpn_protos[] = PG_ALPN_PROTOCOL_VECTOR; + +/* + * Server callback for ALPN negotiation. We use the standard "helper" function + * even though currently we only accept one value. + */ +static int +alpn_cb(SSL *ssl, + const unsigned char **out, + unsigned char *outlen, + const unsigned char *in, + unsigned int inlen, + void *userdata) +{ + /* + * Why does OpenSSL provide a helper function that requires a nonconst + * vector when the callback is declared to take a const vector? What are + * we to do with that? + */ + int retval; + + Assert(userdata != NULL); + Assert(out != NULL); + Assert(outlen != NULL); + Assert(in != NULL); + + retval = SSL_select_next_proto((unsigned char **) out, outlen, + alpn_protos, sizeof(alpn_protos), + in, inlen); + if (*out == NULL || *outlen > sizeof(alpn_protos) || outlen <= 0) + return SSL_TLSEXT_ERR_NOACK; /* can't happen */ + + if (retval == OPENSSL_NPN_NEGOTIATED) + return SSL_TLSEXT_ERR_OK; + else if (retval == OPENSSL_NPN_NO_OVERLAP) + return SSL_TLSEXT_ERR_NOACK; + else + return SSL_TLSEXT_ERR_NOACK; +} + + /* * Set DH parameters for generating ephemeral DH keys. The * DH parameters can take a long time to compute, so they must be diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c index b59df3f660..ee73d01e16 100644 --- a/src/backend/tcop/backend_startup.c +++ b/src/backend/tcop/backend_startup.c @@ -407,6 +407,14 @@ ProcessSSLStartup(Port *port) } Assert(port->ssl_in_use); + if (!port->alpn_used) + { + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("received direct SSL connection request without ALPN protocol negotiation extension"))); + goto reject; + } + if (Trace_connection_negotiation) ereport(LOG, (errmsg("direct SSL connection accepted"))); diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 479f9f2be5..288c1a8c93 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -3882,6 +3882,7 @@ printSSLInfo(void) const char *protocol; const char *cipher; const char *compression; + const char *alpn; if (!PQsslInUse(pset.db)) return; /* no SSL */ @@ -3889,11 +3890,13 @@ printSSLInfo(void) protocol = PQsslAttribute(pset.db, "protocol"); cipher = PQsslAttribute(pset.db, "cipher"); compression = PQsslAttribute(pset.db, "compression"); + alpn = PQsslAttribute(pset.db, "alpn"); - printf(_("SSL connection (protocol: %s, cipher: %s, compression: %s)\n"), + printf(_("SSL connection (protocol: %s, cipher: %s, compression: %s, ALPN: %s)\n"), protocol ? protocol : _("unknown"), cipher ? cipher : _("unknown"), - (compression && strcmp(compression, "off") != 0) ? _("on") : _("off")); + (compression && strcmp(compression, "off") != 0) ? _("on") : _("off"), + alpn ? alpn : _("none")); } /* diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index 4ce61d1b4e..05cb1874c5 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -203,6 +203,7 @@ typedef struct Port char *peer_cn; char *peer_dn; bool peer_cert_valid; + bool alpn_used; /* * OpenSSL structures. (Keep these last so that the locations of other diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index 9ae469c86c..fb93c82053 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -139,6 +139,25 @@ typedef struct CancelRequestPacket uint32 cancelAuthCode; /* secret key to authorize cancel */ } CancelRequestPacket; +/* Application-Layer Protocol Negotiation is required for direct connections + * to avoid protocol confusion attacks (e.g https://alpaca-attack.com/). + * + * ALPN is specified in RFC 7301 + * + * This string should be registered at: + * https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids + * + * OpenSSL uses this wire-format for the list of alpn protocols even in the + * API. Both server and client take the same format parameter but the client + * actually sends it to the server as-is and the server it specifies the + * preference order to use to choose the one selected to send back. + * + * c.f. https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_alpn_select_cb.html + * + * The #define can be used to initialize a char[] vector to use directly in the API + */ +#define PG_ALPN_PROTOCOL "TBD-pgsql" +#define PG_ALPN_PROTOCOL_VECTOR { 9, 'T','B','D','-','p','g','s','q','l' } /* * A client can also start by sending a SSL or GSSAPI negotiation request to diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index a43e74284f..d559ed86e8 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -885,6 +885,9 @@ destroy_ssl_system(void) #endif } +/* See pqcomm.h comments on OpenSSL implementation of ALPN (RFC 7301) */ +static unsigned char alpn_protos[] = PG_ALPN_PROTOCOL_VECTOR; + /* * Create per-connection SSL object, and load the client certificate, * private key, and trusted CA certs. @@ -1233,6 +1236,22 @@ initialize_SSL(PGconn *conn) } } + /* Set ALPN */ + { + int retval; + + retval = SSL_set_alpn_protos(conn->ssl, alpn_protos, sizeof(alpn_protos)); + + if (retval != 0) + { + char *err = SSLerrmessage(ERR_get_error()); + + libpq_append_conn_error(conn, "could not set ssl alpn extension: %s", err); + SSLerrfree(err); + return -1; + } + } + /* * Read the SSL key. If a key is specified, treat it as an engine:key * combination if there is colon present - we don't support files with @@ -1754,6 +1773,7 @@ PQsslAttributeNames(PGconn *conn) "cipher", "compression", "protocol", + "alpn", NULL }; static const char *const empty_attrs[] = {NULL}; @@ -1808,6 +1828,21 @@ PQsslAttribute(PGconn *conn, const char *attribute_name) if (strcmp(attribute_name, "protocol") == 0) return SSL_get_version(conn->ssl); + if (strcmp(attribute_name, "alpn") == 0) + { + const unsigned char *data; + unsigned int len; + static char alpn_str[256]; /* alpn doesn't support longer than 255 + * bytes */ + + SSL_get0_alpn_selected(conn->ssl, &data, &len); + if (data == NULL || len == 0 || len > sizeof(alpn_str) - 1) + return NULL; + memcpy(alpn_str, data, len); + alpn_str[len] = 0; + return alpn_str; + } + return NULL; /* unknown attribute */ }