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
This commit is contained in:
Heikki Linnakangas 2024-04-08 04:24:51 +03:00
parent d39a49c1e4
commit 91044ae4ba
7 changed files with 157 additions and 2 deletions

View File

@ -2944,6 +2944,18 @@ const char *PQsslAttribute(const PGconn *conn, const char *attribute_name);
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><literal>alpn</literal></term>
<listitem>
<para>
Application protocol selected by the TLS Application-Layer
Protocol Negotiation (ALPN) extension. The only protocol
supported by libpq is <literal>TBD-pgsql</literal>, so this is
mainly useful for checking whether the server supported ALPN or
not. Empty string if ALPN was not used.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>

View File

@ -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

View File

@ -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")));

View File

@ -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"));
}
/*

View File

@ -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

View File

@ -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

View File

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