From 3b38ec5fd7a2d4fe5f3f89c2f7cb6ea20b9fa0e5 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Thu, 5 Mar 2015 09:50:52 +0200 Subject: [PATCH 4/4] WIP: Implement SCRAM authentication. --- doc/src/sgml/protocol.sgml | 148 ++++++++- src/backend/commands/user.c | 6 + src/backend/libpq/Makefile | 2 +- src/backend/libpq/auth-scram.c | 621 +++++++++++++++++++++++++++++++++++ src/backend/libpq/auth.c | 110 ++++++- src/backend/libpq/hba.c | 14 + src/backend/libpq/pg_hba.conf.sample | 2 +- src/backend/utils/adt/encode.c | 8 +- src/common/Makefile | 2 +- src/common/scram-common.c | 161 +++++++++ src/include/common/scram-common.h | 35 ++ src/include/libpq/auth.h | 5 + src/include/libpq/crypt.h | 1 + src/include/libpq/hba.h | 1 + src/include/libpq/pqcomm.h | 2 + src/include/libpq/scram.h | 23 ++ src/include/utils/builtins.h | 4 + src/interfaces/libpq/Makefile | 8 +- src/interfaces/libpq/fe-auth-scram.c | 476 +++++++++++++++++++++++++++ src/interfaces/libpq/fe-auth.c | 95 ++++++ src/interfaces/libpq/fe-auth.h | 8 + src/interfaces/libpq/fe-connect.c | 49 +++ src/interfaces/libpq/libpq-int.h | 5 + 23 files changed, 1769 insertions(+), 17 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/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index 3a753a0..aec0e05 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. + + + + @@ -2559,6 +2588,115 @@ 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. + + + + + + + + + @@ -4321,7 +4459,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/src/backend/commands/user.c b/src/backend/commands/user.c index 42e473d..4347399 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 "libpq/md5.h" +#include "libpq/scram.h" #include "miscadmin.h" #include "storage/lmgr.h" #include "utils/acl.h" @@ -1698,6 +1699,11 @@ buildPasswordVerifiers(char *rolname, char *password, bool store_encrypted, elog(ERROR, "password encryption failed"); verifier = psprintf("md5:%s", encrypted_password); datums[nverifiers++] = CStringGetTextDatum(verifier); + + /* Also create a SCRAM verifier */ + verifier = scram_build_verifier(rolname, password, 0); + verifier = psprintf("scram:%s", verifier); + datums[nverifiers++] = CStringGetTextDatum(verifier); } } diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile index 09410c4..3dd60e1 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 ip.o md5.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..060193b --- /dev/null +++ b/src/backend/libpq/auth-scram.c @@ -0,0 +1,621 @@ +/*------------------------------------------------------------------------- + * + * auth-scram.c + * Server-side implementation of the SASL SCRAM mechanism. + * + * See RFC 5802. 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. + * + * The verifier stored in pg_authid consists of the salt, iteration count, + * StoredKey, and ServerKey. + * + * Portions Copyright (c) 1996-2015, 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/scram-common.h" +#include "common/sha1.h" +#include "libpq/auth.h" +#include "libpq/crypt.h" +#include "libpq/scram.h" +#include "miscadmin.h" +#include "utils/builtins.h" + +#define SALT_LEN 10 /* length of salt when generating new + * verifiers */ +#define NONCE_LEN 10 /* length of random nonce generated in the + * authentication exchange */ + +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]; + + /* These come from the client-first message */ + char *client_first_message_bare; + char *client_username; /* username from client-first message */ + char *client_authzid; + char *client_nonce; + + /* These come from the client-final message */ + char *client_final_message_without_proof; + char *client_final_nonce; + char ClientProof[SCRAM_KEY_LEN]; + + 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 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 *v; + char *p; + + state = (scram_state *) palloc0(sizeof(scram_state)); + state->state = INIT; + state->username = username; + + /* + * The verifier is of form: + * + * salt:iterations:storedkey:serverkey + */ + v = pstrdup(verifier); + + /* salt */ + if ((p = strtok(v, ":")) == NULL) + goto invalid_verifier; + state->salt = pstrdup(p); + + /* iterations */ + if ((p = strtok(NULL, ":")) == NULL) + goto invalid_verifier; + errno = 0; + state->iterations = 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; + hex_decode(p, SCRAM_KEY_LEN*2, (char *) state->StoredKey); + + /* serverkey */ + if ((p = strtok(NULL, ":")) == NULL) + goto invalid_verifier; + if (strlen(p) != SCRAM_KEY_LEN * 2) + goto invalid_verifier; + hex_decode(p, SCRAM_KEY_LEN*2, (char *) state->ServerKey); + + pfree(v); + + return state; + +invalid_verifier: + /* FIXME: should we keep this secret from the unauthenticated user? */ + elog(ERROR, "invalid SCRAM verifier: %s", verifier); + return NULL; +} + +/* + * 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(LOG, "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.rolverifiers. + * + * If salt is NULL, a random salt is generated. If iterations is 0, default + * number of iterations is used; + */ +char * +scram_build_verifier(char *username, 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[SALT_LEN]; + char *encoded_salt; + int encoded_len; + + if (iterations <= 0) + iterations = 4096; + + generate_nonce(salt, SALT_LEN); + + encoded_salt = palloc(b64_enc_len(salt, SALT_LEN) + 1); + encoded_len = b64_encode(salt, SALT_LEN, encoded_salt); + encoded_salt[encoded_len] = '\0'; + + /* Calculate StoredKey, and encode it in hex */ + scram_ClientOrServerKey(password, salt, SALT_LEN, iterations, "Client Key", 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, SALT_LEN, iterations, "Server Key", 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); +} + + +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; +} + +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; +} + +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 (FIXME: unescape) */ + 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! */ +} + +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; +} + +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, 20); + 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(LOG, "ClientSignature: %02X%02X", ClientSignature[0], ClientSignature[1]); + elog(LOG, "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(LOG, "client's ClientKey: %02X%02X", ClientKey[0], ClientKey[1]); + elog(LOG, "client's StoredKey: %02X%02X", client_StoredKey[0], client_StoredKey[1]); + elog(LOG, "StoredKey: %02X%02X", state->StoredKey[0], state->StoredKey[1]); + + if (memcmp(client_StoredKey, state->StoredKey, SCRAM_KEY_LEN) != 0) + return false; + + /* Also verify the nonce */ + + return true; +} + + +static char * +build_server_first_message(scram_state *state) +{ + char nonce[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, NONCE_LEN); + + state->server_nonce = palloc(b64_enc_len(nonce, NONCE_LEN) + 1); + encoded_len = b64_encode(nonce, 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; +} + +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(b64_dec_len(value, strlen(value))); + if (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'; + + /* FIXME: check channel_binding field */ +} + + +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, 20); + 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(b64_enc_len((const char *) ServerSignature, SCRAM_KEY_LEN) + 1); + siglen = 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) +{ + /* + * TODO: We reuse the salt generated for MD5 authentication. It's only + * four bytes - we'd really want to use a much longer salt. + */ + memset(result, 0, len); + memcpy(result, MyProcPort->md5Salt, Min(sizeof(MyProcPort->md5Salt), len)); +} diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 81226c2..54151cb 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -27,6 +27,7 @@ #include "libpq/libpq.h" #include "libpq/pqformat.h" #include "libpq/md5.h" +#include "libpq/scram.h" #include "miscadmin.h" #include "replication/walsender.h" #include "storage/ipc.h" @@ -185,6 +186,12 @@ static int CheckRADIUSAuth(Port *port); /*---------------------------------------------------------------- + * SASL authentication + *---------------------------------------------------------------- + */ +static int CheckSASLAuth(Port *port, char **logdetail); + +/*---------------------------------------------------------------- * Global authentication functions *---------------------------------------------------------------- */ @@ -246,6 +253,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; @@ -523,6 +531,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, ""); @@ -691,6 +703,96 @@ recv_and_check_password_packet(Port *port, char **logdetail) return result; } +/*---------------------------------------------------------------- + * SASL authentication system + *---------------------------------------------------------------- + */ +static int +CheckSASLAuth(Port *port, char **logdetail) +{ + int mtype; + StringInfoData buf; + void *scram_opaq; + char *verifier; + char *output = NULL; + int outputlen = 0; + int result; + + /* + * 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.) + * + * FIXME: the FE/BE docs need to updated. + */ + if (PG_PROTOCOL_MAJOR(FrontendProtocol) < 3) + ereport(FATAL, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("SASL authentication is not supported in protocol version 2"))); + + /* lookup verifier */ + verifier = get_role_verifier(port->user_name, "scram", logdetail); + if (verifier == NULL) + return STATUS_ERROR; + + sendAuthRequest(port, AUTH_REQ_SASL, "SCRAM-SHA-1", strlen("SCRAM-SHA-1") + 1); + + scram_opaq = scram_init(port->user_name, verifier); + + /* + * Loop through SASL message exchange. This exchange can consist of + * multiple messags 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); + + return (result == SASL_EXCHANGE_SUCCESS) ? STATUS_OK : STATUS_ERROR; +} /*---------------------------------------------------------------- @@ -891,8 +993,8 @@ pg_GSS_recvauth(Port *port) (unsigned int) port->gss->outbuf.length); /* - * Add the authentication data for the next step of the GSSAPI or - * SSPI negotiation. + * Add the authentication data for the next step of the GSSAPI + * negotiation. */ elog(DEBUG4, "sending GSS token of length %u", (unsigned int) port->gss->outbuf.length); @@ -1142,8 +1244,8 @@ pg_SSPI_recvauth(Port *port) port->gss->outbuf.value = outbuf.pBuffers[0].pvBuffer; /* - * Add the authentication data for the next step of the GSSAPI or - * SSPI negotiation. + * Add the authentication data for the next step of the SSPI + * negotiation. */ elog(DEBUG4, "sending GSS token of length %u", (unsigned int) port->gss->outbuf.length); diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index a0f5396..31bd78d 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -1184,6 +1184,20 @@ parse_hba_line(List *line, int line_num, char *raw_line) } parsedline->auth_method = uaMD5; } + else if (strcmp(token->string, "scram") == 0) + { + /* FIXME: could we support Db_user_namespace with SCRAM? */ + 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..dc3ce2f 100644 --- a/src/backend/libpq/pg_hba.conf.sample +++ b/src/backend/libpq/pg_hba.conf.sample @@ -42,7 +42,7 @@ # 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", +# 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" is preferred since # it sends encrypted passwords. diff --git a/src/backend/utils/adt/encode.c b/src/backend/utils/adt/encode.c index 4b32b6c..c414e86 100644 --- a/src/backend/utils/adt/encode.c +++ b/src/backend/utils/adt/encode.c @@ -214,7 +214,7 @@ static const int8 b64lookup[128] = { 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, }; -static unsigned +unsigned b64_encode(const char *src, unsigned len, char *dst) { char *p, @@ -261,7 +261,7 @@ b64_encode(const char *src, unsigned len, char *dst) return p - dst; } -static unsigned +unsigned b64_decode(const char *src, unsigned len, char *dst) { const char *srcend = src + len, @@ -331,14 +331,14 @@ b64_decode(const char *src, unsigned len, char *dst) } -static unsigned +unsigned b64_enc_len(const char *src, unsigned srclen) { /* 3 bytes will be converted to 4, linefeed after 76 chars */ return (srclen + 2) * 4 / 3 + srclen / (76 * 3 / 4); } -static unsigned +unsigned b64_dec_len(const char *src, unsigned srclen) { return (srclen * 3) >> 2; diff --git a/src/common/Makefile b/src/common/Makefile index f526b42..5b1b8ac 100644 --- a/src/common/Makefile +++ b/src/common/Makefile @@ -24,7 +24,7 @@ override CPPFLAGS := -DFRONTEND $(CPPFLAGS) LIBS += $(PTHREAD_LIBS) OBJS_COMMON = exec.o pg_crc.o pg_lzcompress.o pgfnames.o psprintf.o relpath.o \ - rmtree.o sha1.o string.o username.o wait_error.o + rmtree.o scram-common.o sha1.o string.o username.o wait_error.o OBJS_FRONTEND = $(OBJS_COMMON) fe_memutils.o diff --git a/src/common/scram-common.c b/src/common/scram-common.c new file mode 100644 index 0000000..714fd30 --- /dev/null +++ b/src/common/scram-common.c @@ -0,0 +1,161 @@ +/*------------------------------------------------------------------------- + * 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 + * Mechansim (SCRAM), per IETF's RFC 5802. + * + * Portions Copyright (c) 2015, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/common/scram-common.c + * + *------------------------------------------------------------------------- + */ +#ifndef FRONTEND +#include "postgres.h" +#else +#include "postgres_fe.h" +#endif + +#include "common/scram-common.h" + +/* + * Calculate HMAC per RFC2104. + * + * The hash function used is SHA-1. + */ +void +scram_HMAC_init(scram_HMAC_ctx *ctx, const uint8 *key, int keylen) +{ + uint8 k_ipad[SHA1_HMAC_B]; + int i; + uint8 keybuf[SHA1_RESULTLEN]; + + /* + * If the key is longer than the block size (64 bytes for SHA-1), + * pass it through SHA-1 once to shrink it down + */ + if (keylen > SHA1_HMAC_B) + { + SHA1_CTX sha1_ctx; + + SHA1Init(&sha1_ctx); + SHA1Update(&sha1_ctx, key, keylen); + SHA1Final(keybuf, &sha1_ctx); + key = keybuf; + keylen = SHA1_RESULTLEN; + } + + memset(k_ipad, 0x36, SHA1_HMAC_B); + memset(ctx->k_opad, 0x5C, SHA1_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) */ + SHA1Init(&ctx->sha1ctx); + SHA1Update(&ctx->sha1ctx, k_ipad, SHA1_HMAC_B); +} + +void +scram_HMAC_update(scram_HMAC_ctx *ctx, const const char *str, int slen) +{ + SHA1Update(&ctx->sha1ctx, (const uint8 *) str, slen); +} + +void +scram_HMAC_final(uint8 *result, scram_HMAC_ctx *ctx) +{ + uint8 h[SHA1_RESULTLEN]; + + SHA1Final(h, &ctx->sha1ctx); + + /* H(K XOR opad, tmp) */ + SHA1Init(&ctx->sha1ctx); + SHA1Update(&ctx->sha1ctx, ctx->k_opad, SHA1_HMAC_B); + SHA1Update(&ctx->sha1ctx, h, SHA1_RESULTLEN); + SHA1Final(result, &ctx->sha1ctx); +} + +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-1 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) +{ + SHA1_CTX ctx; + + SHA1Init(&ctx); + SHA1Update(&ctx, input, len); + SHA1Final(result, &ctx); +} + +static void +scram_Normalize(const char *password, char *result) +{ + /* TODO */ + strlcpy(result, password, 20); +} + +static void +scram_SaltedPassword(const char *password, const char *salt, int saltlen, int iterations, + uint8 *result) +{ + /* FIXME: pwbuf needs to be sized properly */ + char pwbuf[20]; + + scram_Normalize(password, pwbuf); + scram_Hi(pwbuf, salt, saltlen, iterations, result); +} + +/* + * 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, 20); + scram_HMAC_update(&ctx, keystr, strlen(keystr)); + scram_HMAC_final(result, &ctx); +} diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h new file mode 100644 index 0000000..fce9337 --- /dev/null +++ b/src/include/common/scram-common.h @@ -0,0 +1,35 @@ +/*------------------------------------------------------------------------- + * + * scram-common.h + * Declarations for helper functions used for SCRAM authentication + * + * Portions Copyright (c) 1996-2015, 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/sha1.h" + +#define SCRAM_KEY_LEN SHA1_RESULTLEN + +#define SHA1_HMAC_B 64 + +typedef struct +{ + SHA1_CTX sha1ctx; + uint8 k_opad[SHA1_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 80f26a8..4469565 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/crypt.h b/src/include/libpq/crypt.h index dfab8f3..77a388e 100644 --- a/src/include/libpq/crypt.h +++ b/src/include/libpq/crypt.h @@ -15,6 +15,7 @@ #include "libpq/libpq-be.h" +extern char *get_role_verifier(const char *rolname, const char *method, char **logdetail); extern int md5_crypt_verify(const Port *port, const char *role, char *client_pass, char **logdetail); diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index 68a953a..a73d2f9 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/pqcomm.h b/src/include/libpq/pqcomm.h index 639bf72..462a5dd 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..f273392 --- /dev/null +++ b/src/include/libpq/scram.h @@ -0,0 +1,23 @@ +/*------------------------------------------------------------------------- + * + * scram.h + * Interface to libpq/scram.c + * + * + * Portions Copyright (c) 1996-2015, 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 + +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(char *username, char *password, + int iterations); + +#endif diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h index 6310641..cad9852 100644 --- a/src/include/utils/builtins.h +++ b/src/include/utils/builtins.h @@ -157,6 +157,10 @@ extern Datum binary_encode(PG_FUNCTION_ARGS); extern Datum binary_decode(PG_FUNCTION_ARGS); extern unsigned hex_encode(const char *src, unsigned len, char *dst); extern unsigned hex_decode(const char *src, unsigned len, char *dst); +extern unsigned b64_encode(const char *src, unsigned len, char *dst); +extern unsigned b64_decode(const char *src, unsigned len, char *dst); +extern unsigned b64_enc_len(const char *src, unsigned srclen); +extern unsigned b64_dec_len(const char *src, unsigned srclen); /* enum.c */ extern Datum enum_in(PG_FUNCTION_ARGS); diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 6973a20..4eae326 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -31,7 +31,7 @@ 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 @@ -43,6 +43,9 @@ OBJS += $(filter crypt.o getaddrinfo.o getpeereid.o inet_aton.o open.o system.o OBJS += ip.o md5.o # utils/mb OBJS += encnames.o wchar.o +# common/ +# FIXME: any reason not to link with libpgcommon? +OBJS += scram-common.o sha1.o ifeq ($(with_openssl),yes) OBJS += fe-secure-openssl.o @@ -102,6 +105,9 @@ ip.c md5.c: % : $(backend_src)/libpq/% encnames.c wchar.c: % : $(backend_src)/utils/mb/% rm -f $@ && $(LN_S) $< . +scram-common.c sha1.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..9165a82 --- /dev/null +++ b/src/interfaces/libpq/fe-auth-scram.c @@ -0,0 +1,476 @@ +/*------------------------------------------------------------------------- + * + * fe-auth-scram.c + * The front-end (client) implementation of SCRAM authentication. + * + * Portions Copyright (c) 1996-2015, 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/scram-common.h" +#include "fe-auth.h" + +typedef struct +{ + enum + { + INIT, + NONCE_SENT, + PROOF_SENT, + FINISHED + } state; + + const char *username; + const char *password; + + char *client_first_message_bare; + + /* 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_proof; +} 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); +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); + +static unsigned b64_decode(const char *src, unsigned len, char *dst); +static unsigned b64_encode(const char *src, unsigned len, char *dst); +static unsigned b64_enc_len(const char *src, unsigned srclen); +static unsigned b64_dec_len(const char *src, unsigned srclen); + +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; +} + +void +pg_fe_scram_free(void *opaq) +{ + fe_scram_state *state = (fe_scram_state *) opaq; + + if (state->client_first_message_bare) + free(state->client_first_message_bare); + + if (state->server_first_message) + free(state->server_first_message); + if (state->salt) + free(state->salt); + if (state->server_nonce) + free(state->server_nonce); + if (state->server_proof) + free(state->server_proof); + + free(state); +} + +/* + * TODO: Even though SCRAM will authenticate the server, by verifying the + * server-proof, we don't currently store that information anywhere. Nothing + * stops the server from leaving the SASL exchange unfinished, and just send an + * AuthenticationOK message, leaving the client unsure of the server's + * identity. + */ +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); + *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"); + } +} + +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; +} + +static char * +build_client_first_message(fe_scram_state *state) +{ + char nonce[10 + 1]; + char *buf; + char msglen; + + generate_nonce(nonce, 10); + + 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; +} + +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(b64_dec_len(encoded_salt, strlen(encoded_salt))); + if (state->salt == NULL) + return false; + state->saltlen = b64_decode(encoded_salt, strlen(encoded_salt), state->salt); + if (state->saltlen <= 0) + 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; +} + +static bool +read_server_final_message(fe_scram_state *state, char *input) +{ + /* TODO: verify ServerSignature */ + return true; +} + +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 (b64_enc_len((char *) client_proof, SCRAM_KEY_LEN) > sizeof(client_proof_base64)) + return NULL; + + client_proof_len = b64_encode((char *) client_proof, SCRAM_KEY_LEN, client_proof_base64); + client_proof_base64[client_proof_len] = '\0'; + + snprintf(buf, sizeof(buf), "%s,p=%s", client_final_message_without_proof, client_proof_base64); + fprintf(stderr, "final_msg: %s", buf); + + return strdup(buf); +} + +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, "Client Key", ClientKey); + scram_H(ClientKey, SCRAM_KEY_LEN, StoredKey); + + scram_HMAC_init(&ctx, StoredKey, 20); + 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); + + fprintf(stderr, "ClientSignature: %02X%02X\n", ClientSignature[0], ClientSignature[1]); + + for (i = 0; i < SCRAM_KEY_LEN; i++) + result[i] = ClientKey[i] ^ ClientSignature[i]; +} + +static bool +verify_server_proof(fe_scram_state *state) +{ + /* TODO */ + return true; +} + + +/* + * TODO: We need a source of randomness. libpq doesn't currently have one. + */ +static void +generate_nonce(char *buf, int len) +{ + int i; + + for (i = 0; i < len; i++) + { + buf[i] = 'x'; + } + buf[i] = '\0'; +} + + +/* + * BASE64 + */ + +static const char _base64[] = +"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static const int8 b64lookup[128] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, +}; + +static unsigned +b64_encode(const char *src, unsigned len, char *dst) +{ + char *p, + *lend = dst + 76; + const char *s, + *end = src + len; + int pos = 2; + uint32 buf = 0; + + s = src; + p = dst; + + while (s < end) + { + buf |= (unsigned char) *s << (pos << 3); + pos--; + s++; + + /* write it out */ + if (pos < 0) + { + *p++ = _base64[(buf >> 18) & 0x3f]; + *p++ = _base64[(buf >> 12) & 0x3f]; + *p++ = _base64[(buf >> 6) & 0x3f]; + *p++ = _base64[buf & 0x3f]; + + pos = 2; + buf = 0; + } + if (p >= lend) + { + *p++ = '\n'; + lend = p + 76; + } + } + if (pos != 2) + { + *p++ = _base64[(buf >> 18) & 0x3f]; + *p++ = _base64[(buf >> 12) & 0x3f]; + *p++ = (pos == 0) ? _base64[(buf >> 6) & 0x3f] : '='; + *p++ = '='; + } + + return p - dst; +} + +static unsigned +b64_decode(const char *src, unsigned len, char *dst) +{ + const char *srcend = src + len, + *s = src; + char *p = dst; + char c; + int b = 0; + uint32 buf = 0; + int pos = 0, + end = 0; + + while (s < srcend) + { + c = *s++; + + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') + continue; + + if (c == '=') + { + /* end sequence */ + if (!end) + { + if (pos == 2) + end = 1; + else if (pos == 3) + end = 2; + else + { + return 0; + //ereport(ERROR, + // (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + // errmsg("unexpected \"=\" while decoding base64 sequence"))); + } + } + b = 0; + } + else + { + b = -1; + if (c > 0 && c < 127) + b = b64lookup[(unsigned char) c]; + if (b < 0) + { + //ereport(ERROR, + // (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + // errmsg("invalid symbol '%c' while decoding base64 sequence", (int) c))); + return 0; + } + } + /* add it to buffer */ + buf = (buf << 6) + b; + pos++; + if (pos == 4) + { + *p++ = (buf >> 16) & 255; + if (end == 0 || end > 1) + *p++ = (buf >> 8) & 255; + if (end == 0 || end > 2) + *p++ = buf & 255; + buf = 0; + pos = 0; + } + } + + if (pos != 0) + { + //ereport(ERROR, + // (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + // errmsg("invalid base64 end sequence"), + // errhint("Input data is missing padding, truncated, or otherwise corrupted."))); + return 0; + } + + return p - dst; +} + + +static unsigned +b64_enc_len(const char *src, unsigned srclen) +{ + /* 3 bytes will be converted to 4, linefeed after 76 chars */ + return (srclen + 2) * 4 / 3 + srclen / (76 * 3 / 4); +} + +static unsigned +b64_dec_len(const char *src, unsigned srclen) +{ + return (srclen * 3) >> 2; +} diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index 8927df4..24c8d4a 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -428,6 +428,74 @@ pg_SSPI_startup(PGconn *conn, int use_negotiate) } #endif /* ENABLE_SSPI */ +static bool +pg_SASL_init(PGconn *conn, const char *auth_mechanism) +{ + /* + * Check the authentication mechanism (only SCRAM-SHA-1 is supported at + * the moment.) + */ + if (strcmp(conn->auth_req_inbuf, "SCRAM-SHA-1") == 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; + } +} + +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. * @@ -696,6 +764,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 8d35767..b1b0294 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 e2a06b3..c14fe73 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -2479,6 +2479,46 @@ keep_going: /* We will come back to here until there is } } #endif + /* Get additional payload, if any */ + if (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 GSSAPI 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 @@ -3035,6 +3075,15 @@ closePGconn(PGconn *conn) conn->sspictx = NULL; } #endif + if (conn->sasl_state) + { + /* + * XXX: if we add support for more authentication mechanisms, 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 2175957..391192b 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -421,7 +421,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 */ -- 2.1.4