From 1a5d7e39ec69f18ead4a220d1b52b8c808b0e0ee Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Mon, 26 Sep 2016 14:51:39 +0900 Subject: [PATCH 6/8] Support for SCRAM-SHA-256 authentication (RFC 5802 and 7677) SHA-256 is used. This commit introduces the basic SASL communication protocol plugged in on top of the existing infrastructure. Note that this feature does not add any grammar extension to CREATE and ALTER ROLE, which is left for a future patch. SCRAM authentication can be enabled via password_encryption that gains a new value: 'scram'. Support for channel binding, aka SCRAM-SHA-256-PLUS is left for later, but there is the necessary infrastructure to support it. --- contrib/passwordcheck/passwordcheck.c | 19 +- doc/src/sgml/catalogs.sgml | 19 +- doc/src/sgml/config.sgml | 13 +- doc/src/sgml/protocol.sgml | 147 +++++- doc/src/sgml/ref/create_role.sgml | 14 +- src/backend/commands/user.c | 18 +- src/backend/libpq/Makefile | 2 +- src/backend/libpq/auth-scram.c | 713 ++++++++++++++++++++++++++ src/backend/libpq/auth.c | 132 +++++ src/backend/libpq/crypt.c | 1 + src/backend/libpq/hba.c | 13 + src/backend/libpq/pg_hba.conf.sample | 8 +- src/backend/postmaster/postmaster.c | 1 + src/backend/utils/misc/guc.c | 1 + src/backend/utils/misc/postgresql.conf.sample | 2 +- src/common/Makefile | 2 +- src/common/scram-common.c | 195 +++++++ src/include/commands/user.h | 3 +- src/include/common/scram-common.h | 51 ++ src/include/libpq/auth.h | 5 + src/include/libpq/hba.h | 1 + src/include/libpq/libpq-be.h | 4 +- src/include/libpq/pqcomm.h | 2 + src/include/libpq/scram.h | 27 + src/interfaces/libpq/.gitignore | 5 + src/interfaces/libpq/Makefile | 17 +- src/interfaces/libpq/fe-auth-scram.c | 414 +++++++++++++++ src/interfaces/libpq/fe-auth.c | 106 ++++ src/interfaces/libpq/fe-auth.h | 8 + src/interfaces/libpq/fe-connect.c | 52 ++ src/interfaces/libpq/libpq-int.h | 5 + src/tools/msvc/Mkvcbuild.pm | 10 +- 32 files changed, 1959 insertions(+), 51 deletions(-) create mode 100644 src/backend/libpq/auth-scram.c create mode 100644 src/common/scram-common.c create mode 100644 src/include/common/scram-common.h create mode 100644 src/include/libpq/scram.h create mode 100644 src/interfaces/libpq/fe-auth-scram.c diff --git a/contrib/passwordcheck/passwordcheck.c b/contrib/passwordcheck/passwordcheck.c index a0db89b..faf7208 100644 --- a/contrib/passwordcheck/passwordcheck.c +++ b/contrib/passwordcheck/passwordcheck.c @@ -22,6 +22,7 @@ #include "commands/user.h" #include "common/md5.h" +#include "libpq/scram.h" #include "fmgr.h" PG_MODULE_MAGIC; @@ -57,7 +58,7 @@ check_password(const char *username, { int namelen = strlen(username); int pwdlen = strlen(password); - char encrypted[MD5_PASSWD_LEN + 1]; + char *encrypted; int i; bool pwd_has_letter, pwd_has_nonletter; @@ -65,6 +66,7 @@ check_password(const char *username, switch (password_type) { case PASSWORD_TYPE_MD5: + case PASSWORD_TYPE_SCRAM: /* * Unfortunately we cannot perform exhaustive checks on encrypted @@ -74,12 +76,23 @@ check_password(const char *username, * * We only check for username = password. */ - if (!pg_md5_encrypt(username, username, namelen, encrypted)) - elog(ERROR, "password encryption failed"); + if (password_type == PASSWORD_TYPE_MD5) + { + encrypted = palloc(MD5_PASSWD_LEN + 1); + if (pg_md5_encrypt(username, username, namelen, encrypted)) + elog(ERROR, "password encryption failed"); + } + else if (password_type == PASSWORD_TYPE_SCRAM) + { + encrypted = scram_build_verifier(username, password, 0); + } + else + Assert(0); /* should not happen */ if (strcmp(password, encrypted) == 0) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("password must not contain user name"))); + pfree(encrypted); break; case PASSWORD_TYPE_PLAINTEXT: diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 29738b0..6b28bef 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1310,13 +1310,18 @@ text Password (possibly encrypted); null if none. If the password - is encrypted, this column will begin with the string md5 - followed by a 32-character hexadecimal MD5 hash. The MD5 hash - will be of the user's password concatenated to their user name. - For example, if user joe has password xyzzy, - PostgreSQL will store the md5 hash of - xyzzyjoe. A password that does not follow that - format is assumed to be unencrypted. + is encrypted with MD5, this column will begin with the string + md5 followed by a 32-character hexadecimal MD5 hash. + The MD5 hash will be of the user's password concatenated to their + user name. For example, if user joe has password + xyzzy, PostgreSQL will store the md5 + hash of xyzzyjoe. If the password is encrypted with + SCRAM-SHA-256, it is built with 4 fields separated by a colon. The + first field is a salt encoded in base-64. The second field is the + number of iterations used to generate the password. The third field + is a stored key, encoded in hexadecimal. The fourth field is a + server key encoded in hexadecimal. A password that does not follow + any of those formats is assumed to be unencrypted. diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index e826c19..bccd98e 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1176,9 +1176,10 @@ include_dir 'conf.d' password is to be encrypted. The default value is md5, which stores the password as an MD5 hash. Setting this to plain stores it in plaintext. on and off are also accepted, as - aliases for md5 and plain, respectively. - - + aliases for md5 and plain, respectively. Setting + this parameter to scram will encrypt the password with + SCRAM-SHA-256. + @@ -1251,8 +1252,10 @@ include_dir 'conf.d' Authentication checks are always done with the server's user name so authentication methods must be configured for the server's user name, not the client's. Because - md5 uses the user name as salt on both the - client and server, md5 cannot be used with + md5uses the user name as salt on both the + client and server, and scram uses the user name as + a portion of the salt used on both the client and server, + md5 and scram cannot be used with db_user_namespace. diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index 3384e73..45d92ab 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -228,11 +228,11 @@ The server then sends an appropriate authentication request message, to which the frontend must reply with an appropriate authentication response message (such as a password). - For all authentication methods except GSSAPI and SSPI, there is at most - one request and one response. In some methods, no response + For all authentication methods except GSSAPI, SSPI and SASL, there is at + most one request and one response. In some methods, no response at all is needed from the frontend, and so no authentication request - occurs. For GSSAPI and SSPI, multiple exchanges of packets may be needed - to complete the authentication. + occurs. For GSSAPI, SSPI and SASL, multiple exchanges of packets may be + needed to complete the authentication. @@ -366,6 +366,35 @@ + + AuthenticationSASL + + + The frontend must now initiate a SASL negotiation, using the SASL + mechanism specified in the message. The frontend will send a + PasswordMessage with the first part of the SASL data stream in + response to this. If further messages are needed, the server will + respond with AuthenticationSASLContinue. + + + + + + AuthenticationSASLContinue + + + This message contains the response data from the previous step + of SASL negotiation (AuthenticationSASL, or a previous + AuthenticationSASLContinue). If the SASL data in this message + indicates more data is needed to complete the authentication, + the frontend must send that data as another PasswordMessage. If + SASL authentication is completed by this message, the server + will next send AuthenticationOk to indicate successful authentication + or ErrorResponse to indicate failure. + + + + @@ -2585,6 +2614,114 @@ AuthenticationGSSContinue (B) + + +AuthenticationSASL (B) + + + + + + + + Byte1('R') + + + + Identifies the message as an authentication request. + + + + + + Int32 + + + + Length of message contents in bytes, including self. + + + + + + Int32(10) + + + + Specifies that SASL authentication is started. + + + + + + String + + + + Name of a SASL authentication mechanism. + + + + + + + + + + + +AuthenticationSASLContinue (B) + + + + + + + + Byte1('R') + + + + Identifies the message as an authentication request. + + + + + + Int32 + + + + Length of message contents in bytes, including self. + + + + + + Int32(11) + + + + Specifies that this message contains SASL-mechanism specific + data. + + + + + + Byten + + + + SASL data, specific to the SASL mechanism being used. + + + + + + + + @@ -4347,7 +4484,7 @@ PasswordMessage (F) Identifies the message as a password response. Note that - this is also used for GSSAPI and SSPI response messages + this is also used for GSSAPI, SSPI and SASL response messages (which is really a design error, since the contained data is not a null-terminated string in that case, but can be arbitrary binary data). diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml index 38cd4c8..93f0763 100644 --- a/doc/src/sgml/ref/create_role.sgml +++ b/doc/src/sgml/ref/create_role.sgml @@ -228,16 +228,16 @@ CREATE ROLE name [ [ WITH ] .) If the - presented password string is already in MD5-encrypted format, - then it is stored encrypted as-is, regardless of whether - ENCRYPTED or UNENCRYPTED is specified - (since the system cannot decrypt the specified encrypted - password string). This allows reloading of encrypted - passwords during dump/restore. + presented password string is already in MD5-encrypted or + SCRAM-encrypted format, then it is stored encrypted as-is, + regardless of whether ENCRYPTED or UNENCRYPTED + is specified (since the system cannot decrypt the specified encrypted + password string). This allows reloading of encrypted passwords + during dump/restore. - Note that older clients might lack support for the MD5 + Note that older clients might lack support for the MD5 or SCRAM authentication mechanism that is needed to work with passwords that are stored encrypted. diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index 7f396c9..8b430ad 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -30,6 +30,7 @@ #include "commands/seclabel.h" #include "commands/user.h" #include "common/md5.h" +#include "libpq/scram.h" #include "miscadmin.h" #include "storage/lmgr.h" #include "utils/acl.h" @@ -69,9 +70,9 @@ have_createrole_privilege(void) /* * Encrypt a password if necessary for insertion in pg_authid. * - * If a password is found as already MD5-encrypted, no error is raised - * to ease the dump and reload of such data. Returns a palloc'ed string - * holding the encrypted password. + * If a password is found as already MD5-encrypted or SCRAM-encrypted, no + * error is raised to ease the dump and reload of such data. Returns a + * palloc'ed string holding the encrypted password. */ static char * encrypt_password(char *password, char *rolname, int passwd_type) @@ -81,11 +82,11 @@ encrypt_password(char *password, char *rolname, int passwd_type) Assert(password != NULL); /* - * If a password is already identified as MD5-encrypted, it is used - * as such. If the password given is not encrypted, adapt it depending - * on the type wanted by the caller of this routine. + * A password already identified as a SCRAM-encrypted or MD5-encrypted + * those are used as such. If the password given is not encrypted, + * adapt it depending on the type wanted by the caller of this routine. */ - if (isMD5(password)) + if (isMD5(password) || is_scram_verifier(password)) res = pstrdup(password); else { @@ -101,6 +102,9 @@ encrypt_password(char *password, char *rolname, int passwd_type) res)) elog(ERROR, "password encryption failed"); break; + case PASSWORD_TYPE_SCRAM: + res = scram_build_verifier(rolname, password, 0); + break; default: Assert(0); /* should not come here */ } diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile index 1bdd8ad..7fa2b02 100644 --- a/src/backend/libpq/Makefile +++ b/src/backend/libpq/Makefile @@ -15,7 +15,7 @@ include $(top_builddir)/src/Makefile.global # be-fsstubs is here for historical reasons, probably belongs elsewhere OBJS = be-fsstubs.o be-secure.o auth.o crypt.o hba.o ifaddr.o pqcomm.o \ - pqformat.o pqmq.o pqsignal.o + pqformat.o pqmq.o pqsignal.o auth-scram.o ifeq ($(with_openssl),yes) OBJS += be-secure-openssl.o diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c new file mode 100644 index 0000000..6f2967f --- /dev/null +++ b/src/backend/libpq/auth-scram.c @@ -0,0 +1,713 @@ +/*------------------------------------------------------------------------- + * + * auth-scram.c + * Server-side implementation of the SASL SCRAM mechanism. + * + * See the following RFCs 5802 and RFC 7666 for more details: + * - RFC 5802: https://tools.ietf.org/html/rfc5802 + * - RFC 7677: https://tools.ietf.org/html/rfc7677 + * + * Here are some differences: + * + * - Username from the authentication exchange is not used. The client + * should send an empty string as the username. + * - Password is not processed with the SASLprep algorithm. + * - Channel binding is not supported yet. + * + * The password stored in pg_authid consists of the salt, iteration count, + * StoredKey and ServerKey. + * + * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/backend/libpq/auth-scram.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include + +#include "catalog/pg_authid.h" +#include "common/base64.h" +#include "common/scram-common.h" +#include "common/sha2.h" +#include "libpq/auth.h" +#include "libpq/crypt.h" +#include "libpq/scram.h" +#include "miscadmin.h" +#include "utils/builtins.h" + +typedef struct +{ + enum + { + INIT, + SALT_SENT, + FINISHED + } state; + + const char *username; /* username from startup packet */ + char *salt; /* base64-encoded */ + int iterations; + uint8 StoredKey[SCRAM_KEY_LEN]; + uint8 ServerKey[SCRAM_KEY_LEN]; + + /* Fields of the first message from client */ + char *client_first_message_bare; + char *client_username; + char *client_authzid; + char *client_nonce; + + /* Fields from the last message from client */ + char *client_final_message_without_proof; + char *client_final_nonce; + char ClientProof[SCRAM_KEY_LEN]; + + /* Server-side status fields */ + char *server_first_message; + char *server_nonce; /* base64-encoded */ + char *server_signature; + +} scram_state; + +static void read_client_first_message(scram_state *state, char *input); +static void read_client_final_message(scram_state *state, char *input); +static char *build_server_first_message(scram_state *state); +static char *build_server_final_message(scram_state *state); +static bool verify_client_proof(scram_state *state); +static bool verify_final_nonce(scram_state *state); +static bool parse_scram_verifier(const char *verifier, char **salt, + int *iterations, char **stored_key, char **server_key); + +static void generate_nonce(char *out, int len); + +/* + * Initialize a new SCRAM authentication exchange, with given username and + * its stored verifier. + */ +void * +scram_init(const char *username, const char *verifier) +{ + scram_state *state; + char *server_key; + char *stored_key; + char *salt; + int iterations; + + + state = (scram_state *) palloc0(sizeof(scram_state)); + state->state = INIT; + state->username = username; + + if (!parse_scram_verifier(verifier, &salt, &iterations, + &stored_key, &server_key)) + { + elog(ERROR, "invalid SCRAM verifier"); + return NULL; + } + + state->salt = salt; + state->iterations = iterations; + memcpy(state->ServerKey, server_key, SCRAM_KEY_LEN); + memcpy(state->StoredKey, stored_key, SCRAM_KEY_LEN); + pfree(stored_key); + pfree(server_key); + return state; +} + +/* + * Continue a SCRAM authentication exchange. + */ +int +scram_exchange(void *opaq, char *input, int inputlen, + char **output, int *outputlen) +{ + scram_state *state = (scram_state *) opaq; + int result; + + *output = NULL; + *outputlen = 0; + + if (inputlen > 0) + elog(DEBUG4, "got SCRAM message: %s", input); + + switch (state->state) + { + case INIT: + /* receive username and client nonce, send challenge */ + read_client_first_message(state, input); + *output = build_server_first_message(state); + *outputlen = strlen(*output); + result = SASL_EXCHANGE_CONTINUE; + state->state = SALT_SENT; + break; + + case SALT_SENT: + /* receive response to challenge and verify it */ + read_client_final_message(state, input); + if (verify_final_nonce(state) && verify_client_proof(state)) + { + *output = build_server_final_message(state); + *outputlen = strlen(*output); + result = SASL_EXCHANGE_SUCCESS; + } + else + { + result = SASL_EXCHANGE_FAILURE; + } + state->state = FINISHED; + break; + + default: + elog(ERROR, "invalid SCRAM exchange state"); + result = 0; + } + + return result; +} + +/* + * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword. + * + * If iterations is 0, default number of iterations is used. The result is + * palloc'd, so caller is responsible for freeing it. + */ +char * +scram_build_verifier(const char *username, const char *password, + int iterations) +{ + uint8 keybuf[SCRAM_KEY_LEN + 1]; + char storedkey_hex[SCRAM_KEY_LEN * 2 + 1]; + char serverkey_hex[SCRAM_KEY_LEN * 2 + 1]; + char salt[SCRAM_SALT_LEN]; + char *encoded_salt; + int encoded_len; + + if (iterations <= 0) + iterations = SCRAM_ITERATIONS_DEFAULT; + + generate_nonce(salt, SCRAM_SALT_LEN); + + encoded_salt = palloc(pg_b64_enc_len(salt, SCRAM_SALT_LEN) + 1); + encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt); + encoded_salt[encoded_len] = '\0'; + + /* Calculate StoredKey, and encode it in hex */ + scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, + iterations, SCRAM_CLIENT_KEY_NAME, keybuf); + scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */ + (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, storedkey_hex); + storedkey_hex[SCRAM_KEY_LEN * 2] = '\0'; + + /* And same for ServerKey */ + scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations, + SCRAM_SERVER_KEY_NAME, keybuf); + (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, serverkey_hex); + serverkey_hex[SCRAM_KEY_LEN * 2] = '\0'; + + return psprintf("%s:%d:%s:%s", encoded_salt, iterations, storedkey_hex, serverkey_hex); +} + + +/* + * Check if given verifier can be used for SCRAM authentication. + * Returns true if it is a SCRAM verifier, and false otherwise. + */ +bool +is_scram_verifier(const char *verifier) +{ + return parse_scram_verifier(verifier, NULL, NULL, NULL, NULL); +} + + +/* + * Parse and validate format of given SCRAM verifier. + */ +static bool +parse_scram_verifier(const char *verifier, char **salt, int *iterations, + char **stored_key, char **server_key) +{ + char *salt_res = NULL; + char *stored_key_res = NULL; + char *server_key_res = NULL; + char *v; + char *p; + int iterations_res; + + /* + * The verifier is of form: + * + * salt:iterations:storedkey:serverkey + */ + v = pstrdup(verifier); + + /* salt */ + if ((p = strtok(v, ":")) == NULL) + goto invalid_verifier; + salt_res = pstrdup(p); + + /* iterations */ + if ((p = strtok(NULL, ":")) == NULL) + goto invalid_verifier; + errno = 0; + iterations_res = strtol(p, &p, 10); + if (*p || errno != 0) + goto invalid_verifier; + + /* storedkey */ + if ((p = strtok(NULL, ":")) == NULL) + goto invalid_verifier; + if (strlen(p) != SCRAM_KEY_LEN * 2) + goto invalid_verifier; + + stored_key_res = (char *) palloc(SCRAM_KEY_LEN); + hex_decode(p, SCRAM_KEY_LEN * 2, stored_key_res); + + /* serverkey */ + if ((p = strtok(NULL, ":")) == NULL) + goto invalid_verifier; + if (strlen(p) != SCRAM_KEY_LEN * 2) + goto invalid_verifier; + server_key_res = (char *) palloc(SCRAM_KEY_LEN); + hex_decode(p, SCRAM_KEY_LEN * 2, server_key_res); + + if (iterations) + *iterations = iterations_res; + if (salt) + *salt = salt_res; + else + pfree(salt_res); + if (stored_key) + *stored_key = stored_key_res; + else + pfree(stored_key_res); + if (server_key) + *server_key = server_key_res; + else + pfree(server_key_res); + pfree(v); + return true; + +invalid_verifier: + if (salt_res) + pfree(salt_res); + if (stored_key_res) + pfree(stored_key_res); + if (server_key_res) + pfree(server_key_res); + pfree(v); + return false; +} + +/* + * Read the value in a given SASL exchange message for given attribute. + */ +static char * +read_attr_value(char **input, char attr) +{ + char *begin = *input; + char *end; + + if (*begin != attr) + elog(ERROR, "malformed SCRAM message (%c expected)", attr); + begin++; + + if (*begin != '=') + elog(ERROR, "malformed SCRAM message (expected = in attr %c)", attr); + begin++; + + end = begin; + while (*end && *end != ',') + end++; + + if (*end) + { + *end = '\0'; + *input = end + 1; + } + else + *input = end; + + return begin; +} + +/* + * Read the next attribute and value in a SASL exchange message. + */ +static char * +read_any_attr(char **input, char *attr_p) +{ + char *begin = *input; + char *end; + char attr = *begin; + + if (!((attr >= 'A' && attr <= 'Z') || + (attr >= 'a' && attr <= 'z'))) + elog(ERROR, "malformed SCRAM message (invalid attribute char)"); + if (attr_p) + *attr_p = attr; + begin++; + + if (*begin != '=') + elog(ERROR, "malformed SCRAM message (expected = in attr %c)", attr); + begin++; + + end = begin; + while (*end && *end != ',') + end++; + + if (*end) + { + *end = '\0'; + *input = end + 1; + } + else + *input = end; + + return begin; +} + +/* + * Read and parse the first message from client in the context of a SASL + * authentication exchange message. + */ +static void +read_client_first_message(scram_state *state, char *input) +{ + input = pstrdup(input); + + /* + * saslname = 1*(value-safe-char / "=2C" / "=3D") + * ;; Conforms to . + * + * authzid = "a=" saslname + * ;; Protocol specific. + * + * username = "n=" saslname + * ;; Usernames are prepared using SASLprep. + * + * gs2-cbind-flag = ("p=" cb-name) / "n" / "y" + * ;; "n" -> client doesn't support channel binding. + * ;; "y" -> client does support channel binding + * ;; but thinks the server does not. + * ;; "p" -> client requires channel binding. + * ;; The selected channel binding follows "p=". + * + * gs2-header = gs2-cbind-flag "," [ authzid ] "," + * ;; GS2 header for SCRAM + * ;; (the actual GS2 header includes an optional + * ;; flag to indicate that the GSS mechanism is not + * ;; "standard", but since SCRAM is "standard", we + * ;; don't include that flag). + * + * client-first-message-bare = + * [reserved-mext ","] + * username "," nonce ["," extensions] + * + * client-first-message = + * gs2-header client-first-message-bare + * + * + * For example: + * n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL + */ + + /* read gs2-cbind-flag */ + switch (*input) + { + case 'n': + /* client does not support channel binding */ + input++; + break; + case 'y': + /* client supports channel binding, but we're not doing it today */ + input++; + break; + case 'p': + /* client requires channel binding. We don't support it */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("channel binding not supported"))); + } + + /* any mandatory extensions would go here. */ + if (*input != ',') + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("mandatory extension %c not supported", *input))); + input++; + + /* read optional authzid (authorization identity) */ + if (*input != ',') + state->client_authzid = read_attr_value(&input, 'a'); + else + input++; + + state->client_first_message_bare = pstrdup(input); + + /* read username */ + state->client_username = read_attr_value(&input, 'n'); + + /* read nonce */ + state->client_nonce = read_attr_value(&input, 'r'); + + /* + * There can be any number of optional extensions after this. We don't + * support any extensions, so ignore them. + */ + while (*input != '\0') + read_any_attr(&input, NULL); + + /* success! */ +} + +/* + * Verify the final nonce contained in the last message received from + * client in an exchange. + */ +static bool +verify_final_nonce(scram_state *state) +{ + int client_nonce_len = strlen(state->client_nonce); + int server_nonce_len = strlen(state->server_nonce); + int final_nonce_len = strlen(state->client_final_nonce); + + if (final_nonce_len != client_nonce_len + server_nonce_len) + return false; + if (memcmp(state->client_final_nonce, state->client_nonce, client_nonce_len) != 0) + return false; + if (memcmp(state->client_final_nonce + client_nonce_len, state->server_nonce, server_nonce_len) != 0) + return false; + + return true; +} + +/* + * Verify the client proof contained in the last message received from + * client in an exchange. + */ +static bool +verify_client_proof(scram_state *state) +{ + uint8 ClientSignature[SCRAM_KEY_LEN]; + uint8 ClientKey[SCRAM_KEY_LEN]; + uint8 client_StoredKey[SCRAM_KEY_LEN]; + scram_HMAC_ctx ctx; + int i; + + /* calculate ClientSignature */ + scram_HMAC_init(&ctx, state->StoredKey, SCRAM_KEY_LEN); + scram_HMAC_update(&ctx, + state->client_first_message_bare, + strlen(state->client_first_message_bare)); + scram_HMAC_update(&ctx, ",", 1); + scram_HMAC_update(&ctx, + state->server_first_message, + strlen(state->server_first_message)); + scram_HMAC_update(&ctx, ",", 1); + scram_HMAC_update(&ctx, + state->client_final_message_without_proof, + strlen(state->client_final_message_without_proof)); + scram_HMAC_final(ClientSignature, &ctx); + elog(DEBUG4, "ClientSignature: %02X%02X", ClientSignature[0], ClientSignature[1]); + elog(DEBUG4, "AuthMessage: %s,%s,%s", state->client_first_message_bare, + state->server_first_message, state->client_final_message_without_proof); + + /* Extract the ClientKey that the client calculated from the proof */ + for (i = 0; i < SCRAM_KEY_LEN; i++) + ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i]; + + /* Hash it one more time, and compare with StoredKey */ + scram_H(ClientKey, SCRAM_KEY_LEN, client_StoredKey); + elog(DEBUG4, "client's ClientKey: %02X%02X", ClientKey[0], ClientKey[1]); + elog(DEBUG4, "client's StoredKey: %02X%02X", client_StoredKey[0], client_StoredKey[1]); + elog(DEBUG4, "StoredKey: %02X%02X", state->StoredKey[0], state->StoredKey[1]); + + if (memcmp(client_StoredKey, state->StoredKey, SCRAM_KEY_LEN) != 0) + return false; + + return true; +} + +/* + * Build the first server-side message sent to the client in a SASL + * communication exchange. + */ +static char * +build_server_first_message(scram_state *state) +{ + char nonce[SCRAM_NONCE_LEN]; + int encoded_len; + + /* + * server-first-message = + * [reserved-mext ","] nonce "," salt "," + * iteration-count ["," extensions] + * + * nonce = "r=" c-nonce [s-nonce] + * ;; Second part provided by server. + * + * c-nonce = printable + * + * s-nonce = printable + * + * salt = "s=" base64 + * + * iteration-count = "i=" posit-number + * ;; A positive number. + * + * Example: + * + * r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096 + */ + generate_nonce(nonce, SCRAM_NONCE_LEN); + + state->server_nonce = palloc(pg_b64_enc_len(nonce, SCRAM_NONCE_LEN) + 1); + encoded_len = pg_b64_encode(nonce, SCRAM_NONCE_LEN, state->server_nonce); + + state->server_nonce[encoded_len] = '\0'; + state->server_first_message = + psprintf("r=%s%s,s=%s,i=%u", + state->client_nonce, state->server_nonce, + state->salt, state->iterations); + + return state->server_first_message; +} + +/* + * Read and parse the final message received from client. + */ +static void +read_client_final_message(scram_state *state, char *input) +{ + char attr; + char *channel_binding; + char *value; + char *begin, *proof; + char *p; + char *client_proof; + + begin = p = pstrdup(input); + + /* + * + * cbind-input = gs2-header [ cbind-data ] + * ;; cbind-data MUST be present for + * ;; gs2-cbind-flag of "p" and MUST be absent + * ;; for "y" or "n". + * + * channel-binding = "c=" base64 + * ;; base64 encoding of cbind-input. + * + * proof = "p=" base64 + * + * client-final-message-without-proof = + * channel-binding "," nonce ["," extensions] + * + * client-final-message = + * client-final-message-without-proof "," proof + */ + channel_binding = read_attr_value(&p, 'c'); + if (strcmp(channel_binding, "biws") != 0) + elog(ERROR, "invalid channel binding input"); + state->client_final_nonce = read_attr_value(&p, 'r'); + + /* ignore optional extensions */ + do + { + proof = p - 1; + value = read_any_attr(&p, &attr); + } while (attr != 'p'); + + client_proof = palloc(pg_b64_dec_len(value, strlen(value))); + if (pg_b64_decode(value, strlen(value), client_proof) != SCRAM_KEY_LEN) + elog(ERROR, "invalid ClientProof"); + memcpy(state->ClientProof, client_proof, SCRAM_KEY_LEN); + pfree(client_proof); + + if (*p != '\0') + elog(ERROR, "malformed SCRAM message (garbage at end of message %c)", attr); + + state->client_final_message_without_proof = palloc(proof - begin + 1); + memcpy(state->client_final_message_without_proof, input, proof - begin); + state->client_final_message_without_proof[proof - begin] = '\0'; + + /* XXX: check channel_binding field if support is added */ +} + +/* + * Build the final server-side message of an exchange. + */ +static char * +build_server_final_message(scram_state *state) +{ + uint8 ServerSignature[SCRAM_KEY_LEN]; + char *server_signature_base64; + int siglen; + scram_HMAC_ctx ctx; + + /* calculate ServerSignature */ + scram_HMAC_init(&ctx, state->ServerKey, SCRAM_KEY_LEN); + scram_HMAC_update(&ctx, + state->client_first_message_bare, + strlen(state->client_first_message_bare)); + scram_HMAC_update(&ctx, ",", 1); + scram_HMAC_update(&ctx, + state->server_first_message, + strlen(state->server_first_message)); + scram_HMAC_update(&ctx, ",", 1); + scram_HMAC_update(&ctx, + state->client_final_message_without_proof, + strlen(state->client_final_message_without_proof)); + scram_HMAC_final(ServerSignature, &ctx); + + server_signature_base64 = palloc(pg_b64_enc_len((const char *) ServerSignature, + SCRAM_KEY_LEN) + 1); + siglen = pg_b64_encode((const char *) ServerSignature, + SCRAM_KEY_LEN, server_signature_base64); + server_signature_base64[siglen] = '\0'; + + /* + * + * server-error = "e=" server-error-value + * + * server-error-value = "invalid-encoding" / + * "extensions-not-supported" / ; unrecognized 'm' value + * "invalid-proof" / + * "channel-bindings-dont-match" / + * "server-does-support-channel-binding" / + * ; server does not support channel binding + * "channel-binding-not-supported" / + * "unsupported-channel-binding-type" / + * "unknown-user" / + * "invalid-username-encoding" / + * ; invalid username encoding (invalid UTF-8 or + * ; SASLprep failed) + * "no-resources" / + * "other-error" / + * server-error-value-ext + * ; Unrecognized errors should be treated as "other-error". + * ; In order to prevent information disclosure, the server + * ; may substitute the real reason with "other-error". + * + * server-error-value-ext = value + * ; Additional error reasons added by extensions + * ; to this document. + * + * verifier = "v=" base64 + * ;; base-64 encoded ServerSignature. + * + * server-final-message = (server-error / verifier) + * ["," extensions] + */ + return psprintf("v=%s", server_signature_base64); +} + +static void +generate_nonce(char *result, int len) +{ + /* Use the salt generated for SASL authentication */ + memset(result, 0, len); + memcpy(result, MyProcPort->SASLSalt, Min(sizeof(MyProcPort->SASLSalt), len)); +} diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 0ba8530..47d398d 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -30,9 +30,11 @@ #include "libpq/crypt.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" +#include "libpq/scram.h" #include "miscadmin.h" #include "replication/walsender.h" #include "storage/ipc.h" +#include "utils/timestamp.h" /*---------------------------------------------------------------- @@ -204,6 +206,12 @@ static int CheckRADIUSAuth(Port *port); /*---------------------------------------------------------------- + * SASL authentication + *---------------------------------------------------------------- + */ +static int CheckSASLAuth(Port *port, char **logdetail); + +/*---------------------------------------------------------------- * Global authentication functions *---------------------------------------------------------------- */ @@ -265,6 +273,7 @@ auth_failed(Port *port, int status, char *logdetail) break; case uaPassword: case uaMD5: + case uaSASL: errstr = gettext_noop("password authentication failed for user \"%s\""); /* We use it to indicate if a .pgpass password failed. */ errcode_return = ERRCODE_INVALID_PASSWORD; @@ -545,6 +554,10 @@ ClientAuthentication(Port *port) status = recv_and_check_password_packet(port, &logdetail); break; + case uaSASL: + status = CheckSASLAuth(port, &logdetail); + break; + case uaPAM: #ifdef USE_PAM status = CheckPAMAuth(port, port->user_name, ""); @@ -719,6 +732,125 @@ recv_and_check_password_packet(Port *port, char **logdetail) return result; } +/*---------------------------------------------------------------- + * SASL authentication system + *---------------------------------------------------------------- + */ +static int +CheckSASLAuth(Port *port, char **logdetail) +{ + int retval = STATUS_ERROR; + int mtype; + StringInfoData buf; + void *scram_opaq; + TimestampTz vuntil = 0; + char *output = NULL; + int outputlen = 0; + int result; + char *passwd; + bool vuntil_null; + + /* + * SASL auth is not supported for protocol versions before 3, because it + * relies on the overall message length word to determine the SASL payload + * size in AuthenticationSASLContinue and PasswordMessage messages. (We + * used to have a hard rule that protocol messages must be parsable + * without relying on the length word, but we hardly care about protocol + * version or older anymore.) + */ + if (PG_PROTOCOL_MAJOR(FrontendProtocol) < 3) + ereport(FATAL, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("SASL authentication is not supported in protocol version 2"))); + + /* fetch details about role needed for password checks */ + if (get_role_details(port->user_name, &passwd, &vuntil, &vuntil_null, + logdetail) != STATUS_OK) + return STATUS_ERROR; + + if (!is_scram_verifier(passwd)) + { + *logdetail = psprintf(_("User \"%s\" does not have a SCRAM password."), + port->user_name); + return STATUS_ERROR; + } + + sendAuthRequest(port, AUTH_REQ_SASL, SCRAM_SHA256_NAME, + strlen(SCRAM_SHA256_NAME) + 1); + + scram_opaq = scram_init(port->user_name, passwd); + + /* + * Loop through SASL message exchange. This exchange can consist of + * multiple messages sent in both directions. First message is always from + * the client. All messages from client to server are password packets + * (type 'p'). + */ + do + { + pq_startmsgread(); + mtype = pq_getbyte(); + if (mtype != 'p') + { + /* Only log error if client didn't disconnect. */ + if (mtype != EOF) + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("expected SASL response, got message type %d", + mtype))); + return STATUS_ERROR; + } + + /* Get the actual SASL token */ + initStringInfo(&buf); + if (pq_getmessage(&buf, PG_MAX_AUTH_TOKEN_LENGTH)) + { + /* EOF - pq_getmessage already logged error */ + pfree(buf.data); + return STATUS_ERROR; + } + + elog(DEBUG4, "Processing received SASL token of length %d", buf.len); + + result = scram_exchange(scram_opaq, buf.data, buf.len, + &output, &outputlen); + + /* input buffer no longer used */ + pfree(buf.data); + + if (outputlen > 0) + { + /* + * Negotiation generated data to be sent to the client. + */ + elog(DEBUG4, "sending SASL response token of length %u", outputlen); + + sendAuthRequest(port, AUTH_REQ_SASL_CONT, output, outputlen); + } + } while (result == SASL_EXCHANGE_CONTINUE); + + + if (result != SASL_EXCHANGE_SUCCESS) + { + *logdetail = psprintf(_("SASL exchange failed for user \"%s\"."), + port->user_name); + return STATUS_ERROR; + } + + /* exchange is completed, check if this is past validuntil */ + if (vuntil_null) + retval = STATUS_OK; + else if (vuntil < GetCurrentTimestamp()) + { + *logdetail = psprintf(_("User \"%s\" has an expired password."), + port->user_name); + retval = STATUS_ERROR; + } + else + retval = STATUS_OK; + + return retval; +} /*---------------------------------------------------------------- diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c index 1c41c57..3c6701b 100644 --- a/src/backend/libpq/crypt.c +++ b/src/backend/libpq/crypt.c @@ -84,6 +84,7 @@ get_role_details(const char *role, *logdetail = psprintf(_("User \"%s\" has an empty password."), role); return STATUS_ERROR; /* empty password */ + } return STATUS_OK; diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index f1e9a38..6fe79d7 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -1183,6 +1183,19 @@ parse_hba_line(List *line, int line_num, char *raw_line) } parsedline->auth_method = uaMD5; } + else if (strcmp(token->string, "scram") == 0) + { + if (Db_user_namespace) + { + ereport(LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("SCRAM authentication is not supported when \"db_user_namespace\" is enabled"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + return NULL; + } + parsedline->auth_method = uaSASL; + } else if (strcmp(token->string, "pam") == 0) #ifdef USE_PAM parsedline->auth_method = uaPAM; diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample index 86a89ed..d7ff9bc 100644 --- a/src/backend/libpq/pg_hba.conf.sample +++ b/src/backend/libpq/pg_hba.conf.sample @@ -42,10 +42,10 @@ # or "samenet" to match any address in any subnet that the server is # directly connected to. # -# METHOD can be "trust", "reject", "md5", "password", "gss", "sspi", -# "ident", "peer", "pam", "ldap", "radius" or "cert". Note that -# "password" sends passwords in clear text; "md5" is preferred since -# it sends encrypted passwords. +# METHOD can be "trust", "reject", "md5", "password", "scram", "gss", +# "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert". Note that +# "password" sends passwords in clear text; "md5" or "scram" are preferred +# since they send encrypted passwords. # # OPTIONS are a set of options for the authentication in the format # NAME=VALUE. The available options depend on the different diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index 8bf69ea..f0eeaed 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -2339,6 +2339,7 @@ ConnCreate(int serverFd) * all backends would end up using the same salt... */ pg_strong_random(port->md5Salt, sizeof(port->md5Salt)); + pg_strong_random(port->SASLSalt, sizeof(port->SASLSalt)); /* * Allocate GSSAPI specific state struct diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 622279b..39f2d1b 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -401,6 +401,7 @@ static const struct config_enum_entry force_parallel_mode_options[] = { static const struct config_enum_entry password_encryption_options[] = { {"plain", PASSWORD_TYPE_PLAINTEXT, false}, {"md5", PASSWORD_TYPE_MD5, false}, + {"scram", PASSWORD_TYPE_SCRAM, false}, {"off", PASSWORD_TYPE_PLAINTEXT, false}, {"on", PASSWORD_TYPE_MD5, false}, {"true", PASSWORD_TYPE_MD5, true}, diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 05b1373..393f0ac 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -85,7 +85,7 @@ #ssl_key_file = 'server.key' # (change requires restart) #ssl_ca_file = '' # (change requires restart) #ssl_crl_file = '' # (change requires restart) -#password_encryption = md5 # md5 or plain +#password_encryption = md5 # md5, scram or plain #db_user_namespace = off #row_security = on diff --git a/src/common/Makefile b/src/common/Makefile index 49e41cf..971ddd5 100644 --- a/src/common/Makefile +++ b/src/common/Makefile @@ -42,7 +42,7 @@ override CPPFLAGS += -DVAL_LIBS="\"$(LIBS)\"" OBJS_COMMON = base64.o config_info.o controldata_utils.o exec.o ip.o \ keywords.o md5.o pg_lzcompress.o pgfnames.o psprintf.o relpath.o \ - rmtree.o string.o username.o wait_error.o + rmtree.o scram-common.o string.o username.o wait_error.o ifeq ($(with_openssl),yes) OBJS_COMMON += sha2_openssl.o diff --git a/src/common/scram-common.c b/src/common/scram-common.c new file mode 100644 index 0000000..fb9a0b8 --- /dev/null +++ b/src/common/scram-common.c @@ -0,0 +1,195 @@ +/*------------------------------------------------------------------------- + * scram-common.c + * Shared frontend/backend code for SCRAM authentication + * + * This contains the common low-level functions needed in both frontend and + * backend, for implement the Salted Challenge Response Authentication + * Mechanism (SCRAM), per IETF's RFC 5802. + * + * Portions Copyright (c) 2016, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/common/scram-common.c + * + *------------------------------------------------------------------------- + */ +#ifndef FRONTEND +#include "postgres.h" +#include "utils/memutils.h" +#else +#include "postgres_fe.h" +#endif + +#include "common/scram-common.h" + +#define HMAC_IPAD 0x36 +#define HMAC_OPAD 0x5C + +/* + * Calculate HMAC per RFC2104. + * + * The hash function used is SHA-256. + */ +void +scram_HMAC_init(scram_HMAC_ctx *ctx, const uint8 *key, int keylen) +{ + uint8 k_ipad[SHA256_HMAC_B]; + int i; + uint8 keybuf[SCRAM_KEY_LEN]; + + /* + * If the key is longer than the block size (64 bytes for SHA-256), + * pass it through SHA-256 once to shrink it down + */ + if (keylen > SHA256_HMAC_B) + { + pg_sha256_ctx sha256_ctx; + + pg_sha256_init(&sha256_ctx); + pg_sha256_update(&sha256_ctx, key, keylen); + pg_sha256_final(&sha256_ctx, keybuf); + key = keybuf; + keylen = SCRAM_KEY_LEN; + } + + memset(k_ipad, HMAC_IPAD, SHA256_HMAC_B); + memset(ctx->k_opad, HMAC_OPAD, SHA256_HMAC_B); + + for (i = 0; i < keylen; i++) + { + k_ipad[i] ^= key[i]; + ctx->k_opad[i] ^= key[i]; + } + + /* tmp = H(K XOR ipad, text) */ + pg_sha256_init(&ctx->sha256ctx); + pg_sha256_update(&ctx->sha256ctx, k_ipad, SHA256_HMAC_B); +} + +/* + * Update HMAC calculation + * The hash function used is SHA-256. + */ +void +scram_HMAC_update(scram_HMAC_ctx *ctx, const char *str, int slen) +{ + pg_sha256_update(&ctx->sha256ctx, (const uint8 *) str, slen); +} + +/* + * Finalize HMAC calculation. + * The hash function used is SHA-256. + */ +void +scram_HMAC_final(uint8 *result, scram_HMAC_ctx *ctx) +{ + uint8 h[SCRAM_KEY_LEN]; + + pg_sha256_final(&ctx->sha256ctx, h); + + /* H(K XOR opad, tmp) */ + pg_sha256_init(&ctx->sha256ctx); + pg_sha256_update(&ctx->sha256ctx, ctx->k_opad, SHA256_HMAC_B); + pg_sha256_update(&ctx->sha256ctx, h, SCRAM_KEY_LEN); + pg_sha256_final(&ctx->sha256ctx, result); +} + +/* + * Iterate hash calculation of HMAC entry using given salt. + * scram_Hi() is essentially PBKDF2 (see RFC2898) with HMAC() as the + * pseudorandom function. + */ +static void +scram_Hi(const char *str, const char *salt, int saltlen, int iterations, uint8 *result) +{ + int str_len = strlen(str); + uint32 one = htonl(1); + int i, j; + uint8 Ui[SCRAM_KEY_LEN]; + uint8 Ui_prev[SCRAM_KEY_LEN]; + scram_HMAC_ctx hmac_ctx; + + /* First iteration */ + scram_HMAC_init(&hmac_ctx, (uint8 *) str, str_len); + scram_HMAC_update(&hmac_ctx, salt, saltlen); + scram_HMAC_update(&hmac_ctx, (char *) &one, sizeof(uint32)); + scram_HMAC_final(Ui_prev, &hmac_ctx); + memcpy(result, Ui_prev, SCRAM_KEY_LEN); + + /* Subsequent iterations */ + for (i = 2; i <= iterations; i++) + { + scram_HMAC_init(&hmac_ctx, (uint8 *) str, str_len); + scram_HMAC_update(&hmac_ctx, (const char *) Ui_prev, SCRAM_KEY_LEN); + scram_HMAC_final(Ui, &hmac_ctx); + for (j = 0; j < SCRAM_KEY_LEN; j++) + result[j] ^= Ui[j]; + memcpy(Ui_prev, Ui, SCRAM_KEY_LEN); + } +} + + +/* + * Calculate SHA-256 hash for a NULL-terminated string. (The NULL terminator is + * not included in the hash). + */ +void +scram_H(const uint8 *input, int len, uint8 *result) +{ + pg_sha256_ctx ctx; + + pg_sha256_init(&ctx); + pg_sha256_update(&ctx, input, len); + pg_sha256_final(&ctx, result); +} + +/* + * Normalize a password for SCRAM authentication. + */ +static void +scram_Normalize(const char *password, char *result) +{ + /* + * XXX: Here SASLprep should be applied on password. However, per RFC5802, + * it is required that the password is encoded in UTF-8, something that is + * not guaranteed in this protocol. We may want to revisit this + * normalization function once encoding functions are available as well + * in the frontend in order to be able to encode properly this string, + * and then apply SASLprep on it. + */ + memcpy(result, password, strlen(password) + 1); +} + +/* + * Encrypt password for SCRAM authentication. This basically applies the + * normalization of the password and a hash calculation using the salt + * value given by caller. + */ +static void +scram_SaltedPassword(const char *password, const char *salt, int saltlen, int iterations, + uint8 *result) +{ + char *pwbuf; + + pwbuf = (char *) malloc(strlen(password) + 1); + scram_Normalize(password, pwbuf); + scram_Hi(pwbuf, salt, saltlen, iterations, result); + free(pwbuf); +} + +/* + * Calculate ClientKey or ServerKey. + */ +void +scram_ClientOrServerKey(const char *password, + const char *salt, int saltlen, int iterations, + const char *keystr, uint8 *result) +{ + uint8 keybuf[SCRAM_KEY_LEN]; + scram_HMAC_ctx ctx; + + scram_SaltedPassword(password, salt, saltlen, iterations, keybuf); + scram_HMAC_init(&ctx, keybuf, SCRAM_KEY_LEN); + scram_HMAC_update(&ctx, keystr, strlen(keystr)); + scram_HMAC_final(result, &ctx); +} diff --git a/src/include/commands/user.h b/src/include/commands/user.h index 102c2a5..1ff441a 100644 --- a/src/include/commands/user.h +++ b/src/include/commands/user.h @@ -23,7 +23,8 @@ typedef enum PasswordType { PASSWORD_TYPE_PLAINTEXT = 0, - PASSWORD_TYPE_MD5 + PASSWORD_TYPE_MD5, + PASSWORD_TYPE_SCRAM } PasswordType; extern int Password_encryption; /* GUC */ diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h new file mode 100644 index 0000000..3cc71e6 --- /dev/null +++ b/src/include/common/scram-common.h @@ -0,0 +1,51 @@ +/*------------------------------------------------------------------------- + * + * scram-common.h + * Declarations for helper functions used for SCRAM authentication + * + * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/common/relpath.h + * + *------------------------------------------------------------------------- + */ +#ifndef SCRAM_COMMON_H +#define SCRAM_COMMON_H + +#include "common/sha2.h" + +/* Length of SCRAM keys (client and server) */ +#define SCRAM_KEY_LEN PG_SHA256_DIGEST_LENGTH + +/* length of HMAC */ +#define SHA256_HMAC_B PG_SHA256_BLOCK_LENGTH + +/* length of random nonce generated in the authentication exchange */ +#define SCRAM_NONCE_LEN 10 +/* length of salt when generating new verifiers */ +#define SCRAM_SALT_LEN 10 +/* default number of iterations when generating verifier */ +#define SCRAM_ITERATIONS_DEFAULT 4096 + +/* Base name of keys used for proof generation */ +#define SCRAM_SERVER_KEY_NAME "Server Key" +#define SCRAM_CLIENT_KEY_NAME "Client Key" + +/* + * Context data for HMAC used in SCRAM authentication. + */ +typedef struct +{ + pg_sha256_ctx sha256ctx; + uint8 k_opad[SHA256_HMAC_B]; +} scram_HMAC_ctx; + +extern void scram_HMAC_init(scram_HMAC_ctx *ctx, const uint8 *key, int keylen); +extern void scram_HMAC_update(scram_HMAC_ctx *ctx, const char *str, int slen); +extern void scram_HMAC_final(uint8 *result, scram_HMAC_ctx *ctx); + +extern void scram_H(const uint8 *str, int len, uint8 *result); +extern void scram_ClientOrServerKey(const char *password, const char *salt, int saltlen, int iterations, const char *keystr, uint8 *result); + +#endif diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h index 3cd06b7..5a02534 100644 --- a/src/include/libpq/auth.h +++ b/src/include/libpq/auth.h @@ -22,6 +22,11 @@ extern char *pg_krb_realm; extern void ClientAuthentication(Port *port); +/* Return codes for SASL authentication functions */ +#define SASL_EXCHANGE_CONTINUE 0 +#define SASL_EXCHANGE_SUCCESS 1 +#define SASL_EXCHANGE_FAILURE 2 + /* Hook for plugins to get control in ClientAuthentication() */ typedef void (*ClientAuthentication_hook_type) (Port *, int); extern PGDLLIMPORT ClientAuthentication_hook_type ClientAuthentication_hook; diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index dc7d257..9c93a6b 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -24,6 +24,7 @@ typedef enum UserAuth uaIdent, uaPassword, uaMD5, + uaSASL, uaGSS, uaSSPI, uaPAM, diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index b91eca5..046e200 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -144,7 +144,9 @@ typedef struct Port * Information that needs to be held during the authentication cycle. */ HbaLine *hba; - char md5Salt[4]; /* Password salt */ + char md5Salt[4]; /* MD5 password salt */ + char SASLSalt[10]; /* SASL password salt, size of + * SCRAM_SALT_LEN */ /* * Information that really has no business at all being in struct Port, diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index c6bbfc2..7db809b 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -172,6 +172,8 @@ extern bool Db_user_namespace; #define AUTH_REQ_GSS 7 /* GSSAPI without wrap() */ #define AUTH_REQ_GSS_CONT 8 /* Continue GSS exchanges */ #define AUTH_REQ_SSPI 9 /* SSPI negotiate without wrap() */ +#define AUTH_REQ_SASL 10 /* SASL */ +#define AUTH_REQ_SASL_CONT 11 /* continue SASL exchange */ typedef uint32 AuthRequest; diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h new file mode 100644 index 0000000..f08750f --- /dev/null +++ b/src/include/libpq/scram.h @@ -0,0 +1,27 @@ +/*------------------------------------------------------------------------- + * + * scram.h + * Interface to libpq/scram.c + * + * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/libpq/scram.h + * + *------------------------------------------------------------------------- + */ +#ifndef PG_SCRAM_H +#define PG_SCRAM_H + +/* Name of SCRAM-SHA-256 per IANA */ +#define SCRAM_SHA256_NAME "SCRAM-SHA-256" + +extern void *scram_init(const char *username, const char *verifier); +extern int scram_exchange(void *opaq, char *input, int inputlen, + char **output, int *outputlen); +extern char *scram_build_verifier(const char *username, + const char *password, + int iterations); +extern bool is_scram_verifier(const char *verifier); + +#endif diff --git a/src/interfaces/libpq/.gitignore b/src/interfaces/libpq/.gitignore index cb96af7..c572e11 100644 --- a/src/interfaces/libpq/.gitignore +++ b/src/interfaces/libpq/.gitignore @@ -1,4 +1,5 @@ /exports.list +/base64.c /chklocale.c /crypt.c /getaddrinfo.c @@ -7,8 +8,12 @@ /inet_net_ntop.c /noblock.c /open.c +/pgsrandom.c /pgstrcasecmp.c /pqsignal.c +/scram-common.c +/sha2.c +/sha2_openssl.c /snprintf.c /strerror.c /strlcpy.c diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index b1789eb..0f3c87b 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -31,21 +31,23 @@ LIBS := $(LIBS:-lpgport=) # We can't use Makefile variables here because the MSVC build system scrapes # OBJS from this file. -OBJS= fe-auth.o fe-connect.o fe-exec.o fe-misc.o fe-print.o fe-lobj.o \ +OBJS= fe-auth.o fe-auth-scram.o fe-connect.o fe-exec.o fe-misc.o fe-print.o fe-lobj.o \ fe-protocol2.o fe-protocol3.o pqexpbuffer.o fe-secure.o \ libpq-events.o # libpgport C files we always use -OBJS += chklocale.o inet_net_ntop.o noblock.o pgstrcasecmp.o pqsignal.o \ - thread.o +OBJS += chklocale.o inet_net_ntop.o noblock.o pgsrandom.o pgstrcasecmp.o \ + pqsignal.o thread.o # libpgport C files that are needed if identified by configure OBJS += $(filter crypt.o getaddrinfo.o getpeereid.o inet_aton.o open.o system.o snprintf.o strerror.o strlcpy.o win32error.o win32setlocale.o, $(LIBOBJS)) # src/backend/utils/mb OBJS += encnames.o wchar.o # src/common -OBJS += ip.o md5.o +OBJS += base64.o ip.o md5.o scram-common.o ifeq ($(with_openssl),yes) -OBJS += fe-secure-openssl.o +OBJS += fe-secure-openssl.o sha2_openssl.o +else +OBJS += sha2.o endif ifeq ($(PORTNAME), cygwin) @@ -93,7 +95,7 @@ backend_src = $(top_srcdir)/src/backend # For some libpgport modules, this only happens if configure decides # the module is needed (see filter hack in OBJS, above). -chklocale.c crypt.c getaddrinfo.c getpeereid.c inet_aton.c inet_net_ntop.c noblock.c open.c system.c pgsleep.c pgstrcasecmp.c pqsignal.c snprintf.c strerror.c strlcpy.c thread.c win32error.c win32setlocale.c: % : $(top_srcdir)/src/port/% +chklocale.c crypt.c getaddrinfo.c getpeereid.c inet_aton.c inet_net_ntop.c noblock.c open.c system.c pgsleep.c pgsrandom.c pgstrcasecmp.c pqsignal.c snprintf.c strerror.c strlcpy.c thread.c win32error.c win32setlocale.c: % : $(top_srcdir)/src/port/% rm -f $@ && $(LN_S) $< . ip.c md5.c: % : $(top_srcdir)/src/common/% @@ -102,6 +104,9 @@ ip.c md5.c: % : $(top_srcdir)/src/common/% encnames.c wchar.c: % : $(backend_src)/utils/mb/% rm -f $@ && $(LN_S) $< . +base64.c scram-common.c sha2.c sha2_openssl.c: % : $(top_srcdir)/src/common/% + rm -f $@ && $(LN_S) $< . + distprep: libpq-dist.rc diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c new file mode 100644 index 0000000..5076589 --- /dev/null +++ b/src/interfaces/libpq/fe-auth-scram.c @@ -0,0 +1,414 @@ +/*------------------------------------------------------------------------- + * + * fe-auth-scram.c + * The front-end (client) implementation of SCRAM authentication. + * + * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq/fe-auth-scram.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include "common/base64.h" +#include "common/scram-common.h" +#include "fe-auth.h" + +/* + * Status of exchange messages used for SCRAM authentication via the + * SASL protocol. + */ +typedef struct +{ + enum + { + INIT, + NONCE_SENT, + PROOF_SENT, + FINISHED + } state; + + const char *username; + const char *password; + + char *client_first_message_bare; + char *client_final_message_without_proof; + + /* These come from the server-first message */ + char *server_first_message; + char *salt; + int saltlen; + int iterations; + char *server_nonce; + + /* These come from the server-final message */ + char *server_final_message; + char ServerProof[SCRAM_KEY_LEN]; +} fe_scram_state; + +static bool read_server_first_message(fe_scram_state *state, + char *input, + PQExpBuffer errormessage); +static bool read_server_final_message(fe_scram_state *state, + char *input, + PQExpBuffer errormessage); +static char *build_client_first_message(fe_scram_state *state); +static char *build_client_final_message(fe_scram_state *state); +static bool verify_server_proof(fe_scram_state *state); +static void generate_nonce(char *buf, int len); +static void calculate_client_proof(fe_scram_state *state, + const char *client_final_message_without_proof, + uint8 *result); + +/* + * Initialize SCRAM exchange status. + */ +void * +pg_fe_scram_init(const char *username, const char *password) +{ + fe_scram_state *state; + + state = (fe_scram_state *) malloc(sizeof(fe_scram_state)); + if (!state) + return NULL; + memset(state, 0, sizeof(fe_scram_state)); + state->state = INIT; + state->username = username; + state->password = password; + + return state; +} + +/* + * Free SCRAM exchange status + */ +void +pg_fe_scram_free(void *opaq) +{ + fe_scram_state *state = (fe_scram_state *) opaq; + + /* client messages */ + if (state->client_first_message_bare) + free(state->client_first_message_bare); + if (state->client_final_message_without_proof) + free(state->client_final_message_without_proof); + + /* first message from server */ + if (state->server_first_message) + free(state->server_first_message); + if (state->salt) + free(state->salt); + if (state->server_nonce) + free(state->server_nonce); + + /* final message from server */ + if (state->server_final_message) + free(state->server_final_message); + + free(state); +} + +/* + * Exchange a SCRAM message with backend. + */ +void +pg_fe_scram_exchange(void *opaq, char *input, int inputlen, + char **output, int *outputlen, + bool *done, bool *success, PQExpBuffer errorMessage) +{ + fe_scram_state *state = (fe_scram_state *) opaq; + + *done = false; + *success = false; + *output = NULL; + *outputlen = 0; + + switch (state->state) + { + case INIT: + /* send client nonce */ + *output = build_client_first_message(state); + *outputlen = strlen(*output); + *done = false; + state->state = NONCE_SENT; + break; + + case NONCE_SENT: + /* receive salt and server nonce, send response */ + read_server_first_message(state, input, errorMessage); + *output = build_client_final_message(state); + *outputlen = strlen(*output); + *done = false; + state->state = PROOF_SENT; + break; + + case PROOF_SENT: + /* receive server proof, and verify it */ + read_server_final_message(state, input, errorMessage); + *success = verify_server_proof(state); + *done = true; + state->state = FINISHED; + break; + + default: + /* shouldn't happen */ + *done = true; + *success = false; + printfPQExpBuffer(errorMessage, "invalid SCRAM exchange state"); + } +} + +/* + * Read value for an attribute part of a SASL message. + */ +static char * +read_attr_value(char **input, char attr, PQExpBuffer errorMessage) +{ + char *begin = *input; + char *end; + + if (*begin != attr) + printfPQExpBuffer(errorMessage, "malformed SCRAM message (%c expected)", attr); + begin++; + + if (*begin != '=') + printfPQExpBuffer(errorMessage, "malformed SCRAM message (expected = in attr %c)", attr); + begin++; + + end = begin; + while (*end && *end != ',') + end++; + + if (*end) + { + *end = '\0'; + *input = end + 1; + } + else + *input = end; + + return begin; +} + +/* + * Build the first exchange message sent by the client. + */ +static char * +build_client_first_message(fe_scram_state *state) +{ + char nonce[SCRAM_NONCE_LEN + 1]; + char *buf; + char msglen; + + generate_nonce(nonce, SCRAM_NONCE_LEN); + + /* Generate message */ + msglen = 5 + strlen(state->username) + 3 + strlen(nonce); + buf = malloc(msglen + 1); + snprintf(buf, msglen + 1, "n,,n=%s,r=%s", state->username, nonce); + + state->client_first_message_bare = strdup(buf + 3); + if (!state->client_first_message_bare) + return NULL; + + return buf; +} + +/* + * Build the final exchange message sent from the client. + */ +static char * +build_client_final_message(fe_scram_state *state) +{ + char client_final_message_without_proof[200]; + uint8 client_proof[SCRAM_KEY_LEN]; + char client_proof_base64[SCRAM_KEY_LEN * 2 + 1]; + int client_proof_len; + char buf[300]; + + snprintf(client_final_message_without_proof, sizeof(client_final_message_without_proof), + "c=biws,r=%s", state->server_nonce); + + calculate_client_proof(state, + client_final_message_without_proof, + client_proof); + if (pg_b64_enc_len((char *) client_proof, SCRAM_KEY_LEN) > sizeof(client_proof_base64)) + return NULL; + + client_proof_len = pg_b64_encode((char *) client_proof, SCRAM_KEY_LEN, client_proof_base64); + client_proof_base64[client_proof_len] = '\0'; + + state->client_final_message_without_proof = + strdup(client_final_message_without_proof); + snprintf(buf, sizeof(buf), "%s,p=%s", + client_final_message_without_proof, + client_proof_base64); + + return strdup(buf); +} + +/* + * Read the first exchange message coming from the server. + */ +static bool +read_server_first_message(fe_scram_state *state, + char *input, + PQExpBuffer errormessage) +{ + char *iterations_str; + char *endptr; + char *encoded_salt; + + state->server_first_message = strdup(input); + if (!state->server_first_message) + return false; + + /* parse the message */ + state->server_nonce = strdup(read_attr_value(&input, 'r', errormessage)); + if (state->server_nonce == NULL) + return false; + + encoded_salt = read_attr_value(&input, 's', errormessage); + if (encoded_salt == NULL) + return false; + state->salt = malloc(pg_b64_dec_len(encoded_salt, strlen(encoded_salt))); + if (state->salt == NULL) + return false; + state->saltlen = pg_b64_decode(encoded_salt, strlen(encoded_salt), state->salt); + if (state->saltlen != SCRAM_SALT_LEN) + return false; + + iterations_str = read_attr_value(&input, 'i', errormessage); + if (iterations_str == NULL) + return false; + state->iterations = strtol(iterations_str, &endptr, 10); + if (*endptr != '\0') + return false; + + if (*input != '\0') + return false; + + return true; +} + +/* + * Read the final exchange message coming from the server. + */ +static bool +read_server_final_message(fe_scram_state *state, + char *input, + PQExpBuffer errormessage) +{ + char *encoded_server_proof; + int server_proof_len; + + state->server_final_message = strdup(input); + if (!state->server_final_message) + return false; + + /* parse the message */ + encoded_server_proof = read_attr_value(&input, 'v', errormessage); + if (encoded_server_proof == NULL) + return false; + + server_proof_len = pg_b64_decode(encoded_server_proof, + strlen(encoded_server_proof), + state->ServerProof); + if (server_proof_len != SCRAM_KEY_LEN) + { + printfPQExpBuffer(errormessage, "invalid ServerProof"); + return false; + } + + if (*input != '\0') + return false; + + return true; +} + +/* + * Calculate the client proof, part of the final exchange message sent + * by the client. + */ +static void +calculate_client_proof(fe_scram_state *state, + const char *client_final_message_without_proof, + uint8 *result) +{ + uint8 StoredKey[SCRAM_KEY_LEN]; + uint8 ClientKey[SCRAM_KEY_LEN]; + uint8 ClientSignature[SCRAM_KEY_LEN]; + int i; + scram_HMAC_ctx ctx; + + scram_ClientOrServerKey(state->password, state->salt, state->saltlen, + state->iterations, SCRAM_CLIENT_KEY_NAME, ClientKey); + scram_H(ClientKey, SCRAM_KEY_LEN, StoredKey); + + scram_HMAC_init(&ctx, StoredKey, SCRAM_KEY_LEN); + scram_HMAC_update(&ctx, + state->client_first_message_bare, + strlen(state->client_first_message_bare)); + scram_HMAC_update(&ctx, ",", 1); + scram_HMAC_update(&ctx, + state->server_first_message, + strlen(state->server_first_message)); + scram_HMAC_update(&ctx, ",", 1); + scram_HMAC_update(&ctx, + client_final_message_without_proof, + strlen(client_final_message_without_proof)); + scram_HMAC_final(ClientSignature, &ctx); + + for (i = 0; i < SCRAM_KEY_LEN; i++) + result[i] = ClientKey[i] ^ ClientSignature[i]; +} + +/* + * Validate the server proof, received as part of the final exchange message + * received from the server. + */ +static bool +verify_server_proof(fe_scram_state *state) +{ + uint8 ServerSignature[SCRAM_KEY_LEN]; + uint8 ServerKey[SCRAM_KEY_LEN]; + scram_HMAC_ctx ctx; + + scram_ClientOrServerKey(state->password, state->salt, state->saltlen, + state->iterations, SCRAM_SERVER_KEY_NAME, + ServerKey); + + /* calculate ServerSignature */ + scram_HMAC_init(&ctx, ServerKey, SCRAM_KEY_LEN); + scram_HMAC_update(&ctx, + state->client_first_message_bare, + strlen(state->client_first_message_bare)); + scram_HMAC_update(&ctx, ",", 1); + scram_HMAC_update(&ctx, + state->server_first_message, + strlen(state->server_first_message)); + scram_HMAC_update(&ctx, ",", 1); + scram_HMAC_update(&ctx, + state->client_final_message_without_proof, + strlen(state->client_final_message_without_proof)); + scram_HMAC_final(ServerSignature, &ctx); + + if (memcmp(ServerSignature, state->ServerProof, SCRAM_KEY_LEN) != 0) + return false; + + return true; +} + +/* + * Generate nonce with some randomness. + */ +static void +generate_nonce(char *buf, int len) +{ + pg_strong_random(buf, len); + buf[len] = '\0'; +} diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index 404bc93..97861a7 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -41,6 +41,7 @@ #include "common/md5.h" #include "libpq-fe.h" #include "fe-auth.h" +#include "libpq/scram.h" #ifdef ENABLE_GSS @@ -431,6 +432,84 @@ pg_SSPI_startup(PGconn *conn, int use_negotiate) #endif /* ENABLE_SSPI */ /* + * Initialize SASL status. + * This will be used afterwards for the exchange message protocol used by + * SASL for SCRAM. + */ +static bool +pg_SASL_init(PGconn *conn, const char *auth_mechanism) +{ + /* + * Check the authentication mechanism (only SCRAM-SHA-256 is supported at + * the moment.) + */ + if (strcmp(auth_mechanism, SCRAM_SHA256_NAME) == 0) + { + conn->password_needed = true; + if (conn->pgpass == NULL || conn->pgpass[0] == '\0') + { + printfPQExpBuffer(&conn->errorMessage, + PQnoPasswordSupplied); + return STATUS_ERROR; + } + conn->sasl_state = pg_fe_scram_init(conn->pguser, conn->pgpass); + if (!conn->sasl_state) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("out of memory\n")); + return STATUS_ERROR; + } + else + return STATUS_OK; + } + else + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("SASL authentication mechanism %s not supported\n"), + (char *) conn->auth_req_inbuf); + return STATUS_ERROR; + } +} + +/* + * Exchange a message for SASL communication protocol with the backend. + * This should be used after calling pg_SASL_init to set up the status of + * the protocol. + */ +static int +pg_SASL_exchange(PGconn *conn) +{ + char *output; + int outputlen; + bool done; + bool success; + int res; + + pg_fe_scram_exchange(conn->sasl_state, + conn->auth_req_inbuf, conn->auth_req_inlen, + &output, &outputlen, + &done, &success, &conn->errorMessage); + if (outputlen != 0) + { + /* + * Send the SASL response to the server. We don't care if it's the + * first or subsequent packet, just send the same kind of password + * packet. + */ + res = pqPacketSend(conn, 'p', output, outputlen); + free(output); + + if (res != STATUS_OK) + return STATUS_ERROR; + } + + if (done && !success) + return STATUS_ERROR; + + return STATUS_OK; +} + +/* * Respond to AUTH_REQ_SCM_CREDS challenge. * * Note: this is dead code as of Postgres 9.1, because current backends will @@ -698,6 +777,33 @@ pg_fe_sendauth(AuthRequest areq, PGconn *conn) } break; + case AUTH_REQ_SASL: + /* + * The request contains the name (as assigned by IANA) of the + * authentication mechanism. + */ + if (pg_SASL_init(conn, conn->auth_req_inbuf) != STATUS_OK) + { + /* pg_SASL_init already set the error message */ + return STATUS_ERROR; + } + /* fall through */ + + case AUTH_REQ_SASL_CONT: + if (conn->sasl_state == NULL) + { + printfPQExpBuffer(&conn->errorMessage, + "fe_sendauth: invalid authentication request from server: AUTH_REQ_SASL_CONT without AUTH_REQ_SASL\n"); + return STATUS_ERROR; + } + if (pg_SASL_exchange(conn) != STATUS_OK) + { + printfPQExpBuffer(&conn->errorMessage, + "fe_sendauth: error sending password authentication\n"); + return STATUS_ERROR; + } + break; + case AUTH_REQ_SCM_CREDS: if (pg_local_sendauth(conn) != STATUS_OK) return STATUS_ERROR; diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h index 9d11654..f779fb2 100644 --- a/src/interfaces/libpq/fe-auth.h +++ b/src/interfaces/libpq/fe-auth.h @@ -18,7 +18,15 @@ #include "libpq-int.h" +/* Prototypes for functions in fe-auth.c */ extern int pg_fe_sendauth(AuthRequest areq, PGconn *conn); 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); +extern void pg_fe_scram_free(void *opaq); +extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen, + char **output, int *outputlen, + bool *done, bool *success, PQExpBuffer errorMessage); + #endif /* FE_AUTH_H */ diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index f3a9e5a..6e1ccd6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -2485,6 +2485,49 @@ keep_going: /* We will come back to here until there is } } #endif + /* Get additional payload for SASL, if any */ + if ((areq == AUTH_REQ_SASL || + areq == AUTH_REQ_SASL_CONT) && + msgLength > 4) + { + int llen = msgLength - 4; + + /* + * We can be called repeatedly for the same buffer. Avoid + * re-allocating the buffer in this case - just re-use the + * old buffer. + */ + if (llen != conn->auth_req_inlen) + { + if (conn->auth_req_inbuf) + { + free(conn->auth_req_inbuf); + conn->auth_req_inbuf = NULL; + } + + conn->auth_req_inlen = llen; + conn->auth_req_inbuf = malloc(llen + 1); + if (!conn->auth_req_inbuf) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("out of memory allocating SASL buffer (%d)"), + llen); + goto error_return; + } + } + + if (pqGetnchar(conn->auth_req_inbuf, llen, conn)) + { + /* We'll come back when there is more data. */ + return PGRES_POLLING_READING; + } + + /* + * For safety and convenience, always ensure the in-buffer + * is NULL-terminated. + */ + conn->auth_req_inbuf[llen] = '\0'; + } /* * OK, we successfully read the message; mark data consumed @@ -3042,6 +3085,15 @@ closePGconn(PGconn *conn) conn->sspictx = NULL; } #endif + if (conn->sasl_state) + { + /* + * XXX: if support for more authentication mechanisms is added, this + * needs to call the right 'free' function. + */ + pg_fe_scram_free(conn->sasl_state); + conn->sasl_state = NULL; + } } /* diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index be6c370..7f28d12 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -422,7 +422,12 @@ struct pg_conn PGresult *result; /* result being constructed */ PGresult *next_result; /* next result (used in single-row mode) */ + /* Buffer to hold incoming authentication request data */ + char *auth_req_inbuf; + int auth_req_inlen; + /* Assorted state for SSL, GSS, etc */ + void *sasl_state; #ifdef USE_SSL bool allow_ssl_try; /* Allowed to try SSL negotiation */ diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm index 6748186..39c9b3e 100644 --- a/src/tools/msvc/Mkvcbuild.pm +++ b/src/tools/msvc/Mkvcbuild.pm @@ -112,7 +112,7 @@ sub mkvcbuild our @pgcommonallfiles = qw( base64.c config_info.c controldata_utils.c exec.c ip.c keywords.c md5.c pg_lzcompress.c pgfnames.c psprintf.c relpath.c rmtree.c - string.c username.c wait_error.c); + scram-common.c string.c username.c wait_error.c); if ($solution->{options}->{openssl}) { @@ -233,10 +233,16 @@ sub mkvcbuild $libpq->AddReference($libpgport); # The OBJS scraper doesn't know about ifdefs, so remove fe-secure-openssl.c - # if building without OpenSSL + # and sha2_openssl.c if building without OpenSSL, and remove sha2.c if + # building with OpenSSL. if (!$solution->{options}->{openssl}) { $libpq->RemoveFile('src/interfaces/libpq/fe-secure-openssl.c'); + $libpq->RemoveFile('src/common/sha2_openssl.c'); + } + else + { + $libpq->RemoveFile('src/common/sha2.c'); } my $libpqwalreceiver = -- 2.10.1