From 5da12f90e4c4577933f95205d71f62f906694693 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Tue, 20 Jun 2017 12:51:10 +0900 Subject: [PATCH 4/4] Implement channel binding tls-server-end-point for SCRAM As referenced in RFC 5929, this channel binding is not the default value and uses a hash of the certificate as binding data. On the frontend, this can be resumed in getting the data from SSL_get_peer_certificate() and on the backend SSL_get_certificate(). The hashing algorithm needs also to switch to SHA-256 if the signature algorithm is MD5 or SHA-1, so let's be careful about that. --- doc/src/sgml/protocol.sgml | 4 +- src/backend/libpq/auth-scram.c | 21 ++++++++-- src/backend/libpq/auth.c | 13 ++++-- src/backend/libpq/be-secure-openssl.c | 69 ++++++++++++++++++++++++++++++++ src/include/libpq/libpq-be.h | 1 + src/include/libpq/scram.h | 4 +- src/interfaces/libpq/fe-auth-scram.c | 20 ++++++++- src/interfaces/libpq/fe-auth.c | 18 ++++++++- src/interfaces/libpq/fe-auth.h | 3 +- src/interfaces/libpq/fe-secure-openssl.c | 66 ++++++++++++++++++++++++++++++ src/interfaces/libpq/libpq-int.h | 1 + src/test/ssl/t/002_sasl.pl | 6 ++- 12 files changed, 210 insertions(+), 16 deletions(-) diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index 1863c0905c..1eebcbffa7 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -1433,8 +1433,8 @@ the password is in. Channel binding is supported in builds with SSL support, and uses as mechanism name SCRAM-SHA-256-PLUS for this purpose as -defined per IANA. The only channel binding type supported by the server -is tls-unique. +defined per IANA. The channel binding types supported by the server +are tls-unique, the default, and tls-server-end-point. diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c index f32cf26a37..8bd5cdace5 100644 --- a/src/backend/libpq/auth-scram.c +++ b/src/backend/libpq/auth-scram.c @@ -113,6 +113,8 @@ typedef struct bool ssl_in_use; char *tls_finish_message; int tls_finish_len; + char *certificate_hash; + int certificate_hash_len; char *channel_binding; int iterations; @@ -175,7 +177,9 @@ pg_be_scram_init(const char *username, const char *shadow_pass, bool ssl_in_use, char *tls_finish_message, - int tls_finish_len) + int tls_finish_len, + char *certificate_hash, + int certificate_hash_len) { scram_state *state; bool got_verifier; @@ -186,6 +190,8 @@ pg_be_scram_init(const char *username, state->ssl_in_use = ssl_in_use; state->tls_finish_message = tls_finish_message; state->tls_finish_len = tls_finish_len; + state->certificate_hash = certificate_hash; + state->certificate_hash_len = certificate_hash_len; state->channel_binding = NULL; /* @@ -847,11 +853,12 @@ read_client_first_message(scram_state *state, char *input) errmsg("client supports SCRAM channel binding, but server does not need it for non-SSL connections"))); /* - * Read value provided by client, only tls-unique is supported - * for now. + * Read value provided by client, only tls-unique and + * tls-server-end-point are supported for now. */ channel_name = read_attr_value(&input, 'p'); - if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0) + if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0 && + strcmp(channel_name, SCRAM_CHANNEL_TLS_ENDPOINT) != 0) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), (errmsg("unexpected SCRAM channel-binding type")))); @@ -1120,6 +1127,12 @@ read_client_final_message(scram_state *state, char *input) raw_data = state->tls_finish_message; raw_data_len = state->tls_finish_len; } + else if (strcmp(state->channel_binding, + SCRAM_CHANNEL_TLS_ENDPOINT) == 0) + { + raw_data = state->certificate_hash; + raw_data_len = state->certificate_hash_len; + } else { /* should not happen */ diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index ee9bce03a2..bbab65b64b 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -867,6 +867,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail) bool initial; char *tls_finish = NULL; int tls_finish_len = 0; + char *certificate_bash = NULL; + int certificate_bash_len = 0; /* * SASL auth is not supported for protocol versions before 3, because it @@ -918,12 +920,15 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail) #ifdef USE_SSL /* - * Fetch the data related to the SSL finish message to be used in the - * exchange. + * Fetch the data related to the SSL finish message and the client + * certificate (if any) to be used in the exchange. */ if (port->ssl_in_use) { tls_finish = be_tls_get_peer_finish(port, &tls_finish_len); + certificate_bash = + be_tls_get_certificate_hash(port, + &certificate_bash_len); } #endif @@ -942,7 +947,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail) shadow_pass, port->ssl_in_use, tls_finish, - tls_finish_len); + tls_finish_len, + certificate_bash, + certificate_bash_len); /* * Loop through SASL message exchange. This exchange can consist of diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c index d063a587d0..cea60c6741 100644 --- a/src/backend/libpq/be-secure-openssl.c +++ b/src/backend/libpq/be-secure-openssl.c @@ -1239,6 +1239,75 @@ be_tls_get_peer_finish(Port *port, int *len) return result; } +/* + * Get the server certificate hash for authentication purposes. Per + * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes + * need to be hashed with SHA-256 if its signature algorithm is MD5 or + * SHA-1. If SHA-256 or something else is used, the same hash as the + * signature algorithm is used. + * The result is a palloc'd hash of the server certificate with its + * size, and NULL if there is nothing certificates available. + */ +char * +be_tls_get_certificate_hash(Port *port, int *len) +{ + char *cert_hash = NULL; + X509 *server_cert; + + *len = 0; + server_cert = SSL_get_certificate(port->ssl); + + if (server_cert != NULL) + { + const EVP_MD *algo_type = NULL; + char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */ + unsigned int hash_size; + int algo_nid; + + /* + * 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 find signature algorithm"); + + switch (algo_nid) + { + case NID_sha512: + algo_type = EVP_sha512(); + break; + + case NID_sha384: + algo_type = EVP_sha384(); + break; + + /* + * Fallback to SHA-256 for weaker hashes, and keep them listed + * here for reference. + */ + case NID_md5: + case NID_sha1: + case NID_sha224: + case NID_sha256: + default: + algo_type = EVP_sha256(); + break; + } + + /* generate and save the certificate hash */ + if (!X509_digest(server_cert, algo_type, (unsigned char *) hash, + &hash_size)) + elog(ERROR, "could not generate server certificate hash"); + + cert_hash = (char *) 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/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index 5d59d79822..0392860a87 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_finish(Port *port, int *len); +extern char *be_tls_get_certificate_hash(Port *port, int *len); #endif extern ProtocolVersion FrontendProtocol; diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h index 91cebe9a9b..55d0568d43 100644 --- a/src/include/libpq/scram.h +++ b/src/include/libpq/scram.h @@ -19,6 +19,7 @@ /* Channel binding names */ #define SCRAM_CHANNEL_TLS_UNIQUE "tls-unique" +#define SCRAM_CHANNEL_TLS_ENDPOINT "tls-server-end-point" /* Status codes for message exchange */ #define SASL_EXCHANGE_CONTINUE 0 @@ -28,7 +29,8 @@ /* Routines dedicated to authentication */ extern void *pg_be_scram_init(const char *username, const char *shadow_pass, bool ssl_in_use, char *tls_finish_message, - int tls_finish_len); + int tls_finish_len, char *certificate_hash, + int certificate_hash_len); extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen, char **output, int *outputlen, char **logdetail); diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c index 9be876eac6..d9a403de8a 100644 --- a/src/interfaces/libpq/fe-auth-scram.c +++ b/src/interfaces/libpq/fe-auth-scram.c @@ -48,6 +48,8 @@ typedef struct bool ssl_in_use; char *tls_finish_message; int tls_finish_len; + char *certificate_hash; + int certificate_hash_len; /* enforceable user parameters */ char *saslchannelbinding; /* name of channel binding to use */ @@ -92,7 +94,9 @@ pg_fe_scram_init(const char *username, bool ssl_in_use, char *saslchannelbinding, char *tls_finish_message, - int tls_finish_len) + int tls_finish_len, + char *certificate_hash, + int certificate_hash_len) { fe_scram_state *state; char *prep_password; @@ -107,6 +111,8 @@ pg_fe_scram_init(const char *username, state->ssl_in_use = ssl_in_use; state->tls_finish_message = tls_finish_message; state->tls_finish_len = tls_finish_len; + state->certificate_hash = certificate_hash; + state->certificate_hash_len = certificate_hash_len; /* * If user has specified a channel binding to use, enforce the @@ -150,6 +156,8 @@ pg_fe_scram_free(void *opaq) free(state->password); if (state->tls_finish_message) free(state->tls_finish_message); + if (state->certificate_hash) + free(state->certificate_hash); if (state->saslchannelbinding) free(state->saslchannelbinding); @@ -423,7 +431,9 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage) * Construct client-final-message-without-proof. We need to remember it * for verifying the server proof in the final step of authentication. * Client needs to provide a b64 encoded string of the TLS finish message - * only if a SSL connection is attempted. + * only if a SSL connection is attempted using "tls-unique" as channel + * binding. For "tls-server-end-point", a hash of the client certificate + * is sent instead. */ #ifdef USE_SSL if (state->ssl_in_use) @@ -436,6 +446,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage) raw_data = state->tls_finish_message; raw_data_len = state->tls_finish_len; } + else if (strcmp(state->saslchannelbinding, + SCRAM_CHANNEL_TLS_ENDPOINT) == 0) + { + raw_data = state->certificate_hash; + raw_data_len = state->certificate_hash_len; + } else { /* should not happen */ diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index 4b26018f65..d72d35670f 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -539,6 +539,8 @@ pg_SASL_init(PGconn *conn, int payloadlen) char *password; char *tls_finish = NULL; int tls_finish_len = 0; + char *certificate_hash = NULL; + int certificate_hash_len = 0; conn->password_needed = true; password = conn->connhost[conn->whichhost].password; @@ -552,12 +554,21 @@ pg_SASL_init(PGconn *conn, int payloadlen) } #ifdef USE_SSL - /* Fetch information about the TLS finish message */ + /* + * Fetch information about the TLS finish message and client + * certificate if any. + */ if (conn->ssl_in_use) { tls_finish = pgtls_get_finish(conn, &tls_finish_len); if (tls_finish == NULL) goto oom_error; + + certificate_hash = + pgtls_get_peer_certificate_hash(conn, + &certificate_hash_len); + if (certificate_hash == NULL) + goto oom_error; } #endif @@ -566,7 +577,10 @@ pg_SASL_init(PGconn *conn, int payloadlen) conn->ssl_in_use, conn->saslchannelbinding, tls_finish, - tls_finish_len); + tls_finish_len, + certificate_hash, + certificate_hash_len); + if (!conn->sasl_state) goto oom_error; diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h index 3c699959e0..8db86f8bcc 100644 --- a/src/interfaces/libpq/fe-auth.h +++ b/src/interfaces/libpq/fe-auth.h @@ -25,7 +25,8 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage); /* Prototypes for functions in fe-auth-scram.c */ extern void *pg_fe_scram_init(const char *username, const char *password, bool ssl_in_use, char *saslchannelbinding, - char *tls_finish_message, int tls_finish_len); + char *tls_finish_message, int tls_finish_len, + char *certificate_hash, int certificate_hash_len); extern void pg_fe_scram_free(void *opaq); extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen, char **output, int *outputlen, diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index 84a6e3c322..00a14289e4 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -418,6 +418,72 @@ pgtls_get_finish(PGconn *conn, int *len) return result; } +/* + * Get the hash of the server certificate + * + * This information is useful for end-point channel binding, where + * the client certificate hash is used as a link, per RFC 5929. + */ +char * +pgtls_get_peer_certificate_hash(PGconn *conn, int *len) +{ + char *cert_hash = NULL; + + *len = 0; + + if (conn->peer) + { + X509 *peer_cert = conn->peer; + const EVP_MD *algo_type = NULL; + char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */ + unsigned int hash_size; + int algo_nid; + + /* + * 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)) + return NULL; + + switch (algo_nid) + { + case NID_sha512: + algo_type = EVP_sha512(); + break; + + case NID_sha384: + algo_type = EVP_sha384(); + break; + + /* + * Fallback to SHA-256 for weaker hashes, and keep them listed + * here for reference. + */ + case NID_md5: + case NID_sha1: + case NID_sha224: + case NID_sha256: + default: + algo_type = EVP_sha256(); + break; + } + + if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash, + &hash_size)) + return NULL; + + /* save result */ + cert_hash = (char *) malloc(hash_size); + if (cert_hash == NULL) + 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 6d500aa5db..f1e2c0bb3c 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -673,6 +673,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_finish(PGconn *conn, int *len); +extern char *pgtls_get_peer_certificate_hash(PGconn *conn, int *len); /* * this is so that we can check if a connection is non-blocking internally diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl index a625f0d473..a4e6dfac3d 100644 --- a/src/test/ssl/t/002_sasl.pl +++ b/src/test/ssl/t/002_sasl.pl @@ -2,7 +2,7 @@ use strict; use warnings; use PostgresNode; use TestLib; -use Test::More tests => 6; +use Test::More tests => 8; use ServerSetup; use File::Copy; @@ -44,9 +44,13 @@ test_connect_fails($common_connstr, "saslname=not-exists"); test_connect_fails($common_connstr, "saslname=SCRAM-SHA-256"); test_connect_fails($common_connstr, "saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique"); +test_connect_fails($common_connstr, + "saslname=SCRAM-SHA-256 saslchannelbinding=tls-server-end-point"); # Channel bindings test_connect_ok($common_connstr, "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-unique"); +test_connect_ok($common_connstr, + "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-server-end-point"); test_connect_fails($common_connstr, "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=not-exists"); -- 2.14.1