Send ALPN in TLS handshake, require it in direct SSL connections
authorHeikki Linnakangas <[email protected]>
Mon, 8 Apr 2024 01:24:51 +0000 (04:24 +0300)
committerHeikki Linnakangas <[email protected]>
Mon, 8 Apr 2024 01:24:51 +0000 (04:24 +0300)
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
src/backend/libpq/be-secure-openssl.c
src/backend/tcop/backend_startup.c
src/bin/psql/command.c
src/include/libpq/libpq-be.h
src/include/libpq/pqcomm.h
src/interfaces/libpq/fe-secure-openssl.c

index 0fb728e2b288d5c91b2ec845a7c46546900f9c9c..0306a76161beb2fe99c6863843ee988fea08625b 100644 (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>
 
index 72e43af353780130521ff5272bbe8dbaa2b127ab..6e877c017313af5dc276be7907492c930b8f7366 100644 (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
index b59df3f6603ea641fe8a1b0e96cfd72ccd09fbd2..ee73d01e16c83c8fa88c58538457ec885fe67864 100644 (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")));
index 479f9f2be59ea5edb6bf068ce338975f4b4b68fc..288c1a8c9357cf5822efa109118cf5627fe41ee5 100644 (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"));
 }
 
 /*
index 4ce61d1b4ee17dda5889461d627f7d743bf01dde..05cb1874c580e022332bab298cf797a19c61debe 100644 (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
index 9ae469c86c4202c507fc7d264f4a0ae54a95f26c..fb93c820530d17638ccaa8fc002995c23bdcac81 100644 (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
index a43e74284f2cbaf80a990042330ae1bd1d70565f..d559ed86e8d5d74f962d9836d1e62a29df44f041 100644 (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 */
 }