diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 8174e3defa7..4c5ed1e6d66 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1575,9 +1575,13 @@ the password is in.
Channel binding is supported in PostgreSQL builds with
-SSL support. The SASL mechanism name for SCRAM with channel binding
-is SCRAM-SHA-256-PLUS. The only channel binding type
-supported at the moment is tls-unique, defined in RFC 5929.
+SSL support. The SASL mechanism name for SCRAM with channel binding is
+SCRAM-SHA-256-PLUS. Two channel binding types are
+supported: tls-unique and
+tls-server-end-point, both defined in RFC 5929. Clients
+should use tls-unique if they can support it.
+tls-server-end-point is intended for third-party clients
+that cannot support tls-unique for some reason.
@@ -1597,9 +1601,10 @@ supported at the moment is tls-unique, defined in RFC 5929.
indicates the chosen mechanism, SCRAM-SHA-256 or
SCRAM-SHA-256-PLUS. (A client is free to choose either
mechanism, but for better security it should choose the channel-binding
- variant if it can support it.) In the Initial Client response field,
- the message contains the SCRAM
- client-first-message.
+ variant if it can support it.) In the Initial Client response field, the
+ message contains the SCRAM client-first-message.
+ The client-first-message also contains the channel
+ binding type chosen by the client.
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 1b07eaebfac..48eb531d0f0 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -849,13 +849,14 @@ read_client_first_message(scram_state *state, char *input)
}
/*
- * Read value provided by client; only tls-unique is supported
- * for now. (It is not safe to print the name of an
- * unsupported binding type in the error message. Pranksters
- * could print arbitrary strings into the log that way.)
+ * Read value provided by client. (It is not safe to print
+ * the name of an unsupported binding type in the error
+ * message. Pranksters could print arbitrary strings into the
+ * log that way.)
*/
channel_binding_type = read_attr_value(&input, 'p');
- if (strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) != 0)
+ if (strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) != 0 &&
+ strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_END_POINT) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unsupported SCRAM channel-binding type"))));
@@ -1114,6 +1115,15 @@ read_client_final_message(scram_state *state, char *input)
{
#ifdef USE_SSL
cbind_data = be_tls_get_peer_finished(state->port, &cbind_data_len);
+#endif
+ }
+ else if (strcmp(state->channel_binding_type,
+ SCRAM_CHANNEL_BINDING_TLS_END_POINT) == 0)
+ {
+ /* Fetch hash data of server's SSL certificate */
+#ifdef USE_SSL
+ cbind_data = be_tls_get_certificate_hash(state->port,
+ &cbind_data_len);
#endif
}
else
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 3a7aa018767..f75cc2ef18f 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,67 @@ be_tls_get_peer_finished(Port *port, size_t *len)
return result;
}
+/*
+ * Get the server certificate hash for SCRAM channel binding type
+ * tls-server-end-point.
+ *
+ * The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is no certificate available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, size_t *len)
+{
+ X509 *server_cert;
+ char *cert_hash;
+ const EVP_MD *algo_type = NULL;
+ unsigned char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ *len = 0;
+ server_cert = SSL_get_certificate(port->ssl);
+ if (server_cert == NULL)
+ return NULL;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
+ &algo_nid, NULL))
+ elog(ERROR, "could not determine server certificate signature algorithm");
+
+ /*
+ * The TLS server's certificate bytes need to be hashed with SHA-256 if
+ * its signature algorithm is MD5 or SHA-1 as per RFC 5929
+ * (https://tools.ietf.org/html/rfc5929#section-4.1). If something else
+ * is used, the same hash as the signature algorithm is used.
+ */
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ elog(ERROR, "could not find digest for NID %s",
+ OBJ_nid2sn(algo_nid));
+ break;
+ }
+
+ /* generate and save the certificate hash */
+ if (!X509_digest(server_cert, algo_type, hash, &hash_size))
+ elog(ERROR, "could not generate server certificate hash");
+
+ cert_hash = palloc(hash_size);
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+
+ return cert_hash;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index 3d81934fdab..e1d742ba898 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -21,6 +21,7 @@
/* Channel binding types */
#define SCRAM_CHANNEL_BINDING_TLS_UNIQUE "tls-unique"
+#define SCRAM_CHANNEL_BINDING_TLS_END_POINT "tls-server-end-point"
/* Length of SCRAM keys (client and server) */
#define SCRAM_KEY_LEN PG_SHA256_DIGEST_LENGTH
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index e660e8afa84..49cb2631104 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ extern void be_tls_get_version(Port *port, char *ptr, size_t len);
extern void be_tls_get_cipher(Port *port, char *ptr, size_t len);
extern void be_tls_get_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finished(Port *port, size_t *len);
+extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 06c9cb26141..23bd5fb2b61 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -444,6 +444,21 @@ build_client_final_message(fe_scram_state *state)
cbind_data = pgtls_get_finished(state->conn, &cbind_data_len);
if (cbind_data == NULL)
goto oom_error;
+#endif
+ }
+ else if (strcmp(conn->scram_channel_binding,
+ SCRAM_CHANNEL_BINDING_TLS_END_POINT) == 0)
+ {
+ /* Fetch hash data of server's SSL certificate */
+#ifdef USE_SSL
+ cbind_data =
+ pgtls_get_peer_certificate_hash(state->conn,
+ &cbind_data_len);
+ if (cbind_data == NULL)
+ {
+ /* error message is already set on error */
+ return NULL;
+ }
#endif
}
else
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 7b7390a1fc6..52390640bf3 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -419,6 +419,86 @@ pgtls_get_finished(PGconn *conn, size_t *len)
return result;
}
+/*
+ * Get the hash of the server certificate, for SCRAM channel binding type
+ * tls-server-end-point.
+ *
+ * NULL is sent back to the caller in the event of an error, with an
+ * error message for the caller to consume.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
+{
+ X509 *peer_cert;
+ const EVP_MD *algo_type;
+ unsigned char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+ char *cert_hash;
+
+ *len = 0;
+
+ if (!conn->peer)
+ return NULL;
+
+ peer_cert = conn->peer;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the hash
+ * algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
+ &algo_nid, NULL))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not determine server certificate signature algorithm\n"));
+ return NULL;
+ }
+
+ /*
+ * The TLS server's certificate bytes need to be hashed with SHA-256 if
+ * its signature algorithm is MD5 or SHA-1 as per RFC 5929
+ * (https://tools.ietf.org/html/rfc5929#section-4.1). If something else
+ * is used, the same hash as the signature algorithm is used.
+ */
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find digest for NID %s\n"),
+ OBJ_nid2sn(algo_nid));
+ return NULL;
+ }
+ break;
+ }
+
+ if (!X509_digest(peer_cert, algo_type, hash, &hash_size))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not generate peer certificate hash\n"));
+ return NULL;
+ }
+
+ /* save result */
+ cert_hash = malloc(hash_size);
+ if (cert_hash == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
+ }
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+
+ return cert_hash;
+}
/* ------------------------------------------------------------ */
/* OpenSSL specific code */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 516039eea00..4e354098b39 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -672,6 +672,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finished(PGconn *conn, size_t *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 324b4888d42..3f425e00f0a 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -4,7 +4,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 4;
+use Test::More tests => 5;
use ServerSetup;
use File::Copy;
@@ -45,6 +45,9 @@ test_connect_ok($common_connstr,
test_connect_ok($common_connstr,
"scram_channel_binding=''",
"SCRAM authentication without channel binding");
+test_connect_ok($common_connstr,
+ "scram_channel_binding=tls-server-end-point",
+ "SCRAM authentication with tls-server-end-point as channel binding");
test_connect_fails($common_connstr,
"scram_channel_binding=not-exists",
"SCRAM authentication with invalid channel binding");