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 */ }