From d3fb72ea6de58d285e278459bca9d7cdf7f6a38b Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Thu, 4 Jan 2018 15:18:39 -0500 Subject: [PATCH] Implement channel binding tls-server-end-point for SCRAM This adds a second standard channel binding type for SCRAM. It is mainly intended for third-party clients that cannot implement tls-unique, for example JDBC. Author: Michael Paquier --- doc/src/sgml/protocol.sgml | 17 +++-- src/backend/libpq/auth-scram.c | 20 ++++-- src/backend/libpq/be-secure-openssl.c | 61 ++++++++++++++++++ src/include/common/scram-common.h | 1 + src/include/libpq/libpq-be.h | 1 + src/interfaces/libpq/fe-auth-scram.c | 15 +++++ src/interfaces/libpq/fe-secure-openssl.c | 80 ++++++++++++++++++++++++ src/interfaces/libpq/libpq-int.h | 1 + src/test/ssl/t/002_scram.pl | 5 +- 9 files changed, 189 insertions(+), 12 deletions(-) diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index 8174e3defa..4c5ed1e6d6 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 1b07eaebfa..48eb531d0f 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 3a7aa01876..f75cc2ef18 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 3d81934fda..e1d742ba89 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 e660e8afa8..49cb263110 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 06c9cb2614..23bd5fb2b6 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 7b7390a1fc..52390640bf 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 516039eea0..4e354098b3 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 324b4888d4..3f425e00f0 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");