diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile index 8d1d16b0fc..6d385fd6a4 100644 --- a/src/backend/libpq/Makefile +++ b/src/backend/libpq/Makefile @@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global # be-fsstubs is here for historical reasons, probably belongs elsewhere OBJS = \ + auth-sasl.o \ auth-scram.o \ auth.o \ be-fsstubs.o \ diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c new file mode 100644 index 0000000000..b7cdb2ecf6 --- /dev/null +++ b/src/backend/libpq/auth-sasl.c @@ -0,0 +1,187 @@ +/*------------------------------------------------------------------------- + * + * auth-sasl.c + * Routines to handle network authentication via SASL + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/libpq/auth-sasl.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "libpq/auth.h" +#include "libpq/libpq.h" +#include "libpq/pqformat.h" +#include "libpq/sasl.h" + +/* + * Perform a SASL exchange with a libpq client, using a specific mechanism + * implementation. + * + * shadow_pass is an optional pointer to the shadow entry for the client's + * presented user name. For mechanisms that use shadowed passwords, a NULL + * pointer here means that an entry could not be found for the user (or the user + * does not exist), and the mechanism should fail the authentication exchange. + * + * Mechanisms must take care not to reveal to the client that a user entry does + * not exist; ideally, the external failure mode is identical to that of an + * incorrect password. Mechanisms may instead use the logdetail output parameter + * to internally differentiate between failure cases and assist debugging by the + * server admin. + * + * A mechanism is not required to utilize a shadow entry, or even a password + * system at all; for these cases, shadow_pass may be ignored and the caller + * should just pass NULL. + */ +int +CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass, + char **logdetail) +{ + StringInfoData sasl_mechs; + int mtype; + StringInfoData buf; + void *opaq = NULL; + char *output = NULL; + int outputlen = 0; + const char *input; + int inputlen; + int result; + bool initial; + + /* + * Send the SASL authentication request to user. It includes the list of + * authentication mechanisms that are supported. + */ + initStringInfo(&sasl_mechs); + + mech->get_mechanisms(port, &sasl_mechs); + /* Put another '\0' to mark that list is finished. */ + appendStringInfoChar(&sasl_mechs, '\0'); + + sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs.data, sasl_mechs.len); + pfree(sasl_mechs.data); + + /* + * 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'). + */ + initial = true; + do + { + pq_startmsgread(); + mtype = pq_getbyte(); + if (mtype != 'p') + { + /* Only log error if client didn't disconnect. */ + if (mtype != EOF) + { + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("expected SASL response, got message type %d", + mtype))); + } + else + return STATUS_EOF; + } + + /* Get the actual SASL message */ + initStringInfo(&buf); + if (pq_getmessage(&buf, PG_MAX_SASL_MESSAGE_LENGTH)) + { + /* EOF - pq_getmessage already logged error */ + pfree(buf.data); + return STATUS_ERROR; + } + + elog(DEBUG4, "processing received SASL response of length %d", buf.len); + + /* + * The first SASLInitialResponse message is different from the others. + * It indicates which SASL mechanism the client selected, and contains + * an optional Initial Client Response payload. The subsequent + * SASLResponse messages contain just the SASL payload. + */ + if (initial) + { + const char *selected_mech; + + selected_mech = pq_getmsgrawstring(&buf); + + /* + * Initialize the status tracker for message exchanges. + * + * If the user doesn't exist, or doesn't have a valid password, or + * it's expired, we still go through the motions of SASL + * authentication, but tell the authentication method that the + * authentication is "doomed". That is, it's going to fail, no + * matter what. + * + * This is because we don't want to reveal to an attacker what + * usernames are valid, nor which users have a valid password. + */ + opaq = mech->init(port, selected_mech, shadow_pass); + + inputlen = pq_getmsgint(&buf, 4); + if (inputlen == -1) + input = NULL; + else + input = pq_getmsgbytes(&buf, inputlen); + + initial = false; + } + else + { + inputlen = buf.len; + input = pq_getmsgbytes(&buf, buf.len); + } + pq_getmsgend(&buf); + + /* + * The StringInfo guarantees that there's a \0 byte after the + * response. + */ + Assert(input == NULL || input[inputlen] == '\0'); + + /* + * Hand the incoming message to the mechanism implementation. + */ + result = mech->exchange(opaq, input, inputlen, + &output, &outputlen, + logdetail); + + /* input buffer no longer used */ + pfree(buf.data); + + if (output) + { + /* + * Negotiation generated data to be sent to the client. + */ + elog(DEBUG4, "sending SASL challenge of length %u", outputlen); + + /* TODO: PG_SASL_EXCHANGE_FAILURE with output is forbidden in SASL */ + if (result == PG_SASL_EXCHANGE_SUCCESS) + sendAuthRequest(port, AUTH_REQ_SASL_FIN, output, outputlen); + else + sendAuthRequest(port, AUTH_REQ_SASL_CONT, output, outputlen); + + pfree(output); + } + } while (result == PG_SASL_EXCHANGE_CONTINUE); + + /* Oops, Something bad happened */ + if (result != PG_SASL_EXCHANGE_SUCCESS) + { + return STATUS_ERROR; + } + + return STATUS_OK; +} diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 82f043a343..ac6fe4a747 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -45,19 +45,10 @@ * Global authentication functions *---------------------------------------------------------------- */ -static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata, - int extralen); static void auth_failed(Port *port, int status, char *logdetail); static char *recv_password_packet(Port *port); static void set_authn_id(Port *port, const char *id); -/*---------------------------------------------------------------- - * SASL common authentication - *---------------------------------------------------------------- - */ -static int SASL_exchange(const pg_be_sasl_mech *mech, Port *port, - char *shadow_pass, char **logdetail); - /*---------------------------------------------------------------- * Password-based authentication methods (password, md5, and scram-sha-256) @@ -67,7 +58,6 @@ static int CheckPasswordAuth(Port *port, char **logdetail); static int CheckPWChallengeAuth(Port *port, char **logdetail); static int CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail); -static int CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail); /*---------------------------------------------------------------- @@ -231,14 +221,6 @@ static int PerformRadiusTransaction(const char *server, const char *secret, cons */ #define PG_MAX_AUTH_TOKEN_LENGTH 65535 -/* - * Maximum accepted size of SASL messages. - * - * The messages that the server or libpq generate are much smaller than this, - * but have some headroom. - */ -#define PG_MAX_SASL_MESSAGE_LENGTH 1024 - /*---------------------------------------------------------------- * Global authentication functions *---------------------------------------------------------------- @@ -675,7 +657,7 @@ ClientAuthentication(Port *port) /* * Send an authentication request packet to the frontend. */ -static void +void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata, int extralen) { StringInfoData buf; @@ -855,12 +837,13 @@ CheckPWChallengeAuth(Port *port, char **logdetail) * SCRAM secret, we must do SCRAM authentication. * * If MD5 authentication is not allowed, always use SCRAM. If the user - * had an MD5 password, CheckSCRAMAuth() will fail. + * had an MD5 password, the SCRAM mechanism will fail. */ if (port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5) auth_result = CheckMD5Auth(port, shadow_pass, logdetail); else - auth_result = CheckSCRAMAuth(port, shadow_pass, logdetail); + auth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass, + logdetail); if (shadow_pass) pfree(shadow_pass); @@ -918,159 +901,6 @@ CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail) return result; } -static int -SASL_exchange(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass, - char **logdetail) -{ - StringInfoData sasl_mechs; - int mtype; - StringInfoData buf; - void *opaq = NULL; - char *output = NULL; - int outputlen = 0; - const char *input; - int inputlen; - int result; - bool initial; - - /* - * Send the SASL authentication request to user. It includes the list of - * authentication mechanisms that are supported. - */ - initStringInfo(&sasl_mechs); - - mech->get_mechanisms(port, &sasl_mechs); - /* Put another '\0' to mark that list is finished. */ - appendStringInfoChar(&sasl_mechs, '\0'); - - sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs.data, sasl_mechs.len); - pfree(sasl_mechs.data); - - /* - * 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'). - */ - initial = true; - do - { - pq_startmsgread(); - mtype = pq_getbyte(); - if (mtype != 'p') - { - /* Only log error if client didn't disconnect. */ - if (mtype != EOF) - { - ereport(ERROR, - (errcode(ERRCODE_PROTOCOL_VIOLATION), - errmsg("expected SASL response, got message type %d", - mtype))); - } - else - return STATUS_EOF; - } - - /* Get the actual SASL message */ - initStringInfo(&buf); - if (pq_getmessage(&buf, PG_MAX_SASL_MESSAGE_LENGTH)) - { - /* EOF - pq_getmessage already logged error */ - pfree(buf.data); - return STATUS_ERROR; - } - - elog(DEBUG4, "processing received SASL response of length %d", buf.len); - - /* - * The first SASLInitialResponse message is different from the others. - * It indicates which SASL mechanism the client selected, and contains - * an optional Initial Client Response payload. The subsequent - * SASLResponse messages contain just the SASL payload. - */ - if (initial) - { - const char *selected_mech; - - selected_mech = pq_getmsgrawstring(&buf); - - /* - * Initialize the status tracker for message exchanges. - * - * If the user doesn't exist, or doesn't have a valid password, or - * it's expired, we still go through the motions of SASL - * authentication, but tell the authentication method that the - * authentication is "doomed". That is, it's going to fail, no - * matter what. - * - * This is because we don't want to reveal to an attacker what - * usernames are valid, nor which users have a valid password. - */ - opaq = mech->init(port, selected_mech, shadow_pass); - - inputlen = pq_getmsgint(&buf, 4); - if (inputlen == -1) - input = NULL; - else - input = pq_getmsgbytes(&buf, inputlen); - - initial = false; - } - else - { - inputlen = buf.len; - input = pq_getmsgbytes(&buf, buf.len); - } - pq_getmsgend(&buf); - - /* - * The StringInfo guarantees that there's a \0 byte after the - * response. - */ - Assert(input == NULL || input[inputlen] == '\0'); - - /* - * Hand the incoming message to the mechanism implementation. - */ - result = mech->exchange(opaq, input, inputlen, - &output, &outputlen, - logdetail); - - /* input buffer no longer used */ - pfree(buf.data); - - if (output) - { - /* - * Negotiation generated data to be sent to the client. - */ - elog(DEBUG4, "sending SASL challenge of length %u", outputlen); - - /* TODO: PG_SASL_EXCHANGE_FAILURE with output is forbidden in SASL */ - if (result == PG_SASL_EXCHANGE_SUCCESS) - sendAuthRequest(port, AUTH_REQ_SASL_FIN, output, outputlen); - else - sendAuthRequest(port, AUTH_REQ_SASL_CONT, output, outputlen); - - pfree(output); - } - } while (result == PG_SASL_EXCHANGE_CONTINUE); - - /* Oops, Something bad happened */ - if (result != PG_SASL_EXCHANGE_SUCCESS) - { - return STATUS_ERROR; - } - - return STATUS_OK; -} - -static int -CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail) -{ - return SASL_exchange(&pg_be_scram_mech, port, shadow_pass, logdetail); -} - /*---------------------------------------------------------------- * GSSAPI authentication system diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h index 3610fae3ff..3d6734f253 100644 --- a/src/include/libpq/auth.h +++ b/src/include/libpq/auth.h @@ -21,6 +21,8 @@ extern bool pg_krb_caseins_users; extern char *pg_krb_realm; extern void ClientAuthentication(Port *port); +extern void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata, + int extralen); /* Hook for plugins to get control in ClientAuthentication() */ typedef void (*ClientAuthentication_hook_type) (Port *, int); diff --git a/src/include/libpq/sasl.h b/src/include/libpq/sasl.h index 1afabf843d..dad04d8ecd 100644 --- a/src/include/libpq/sasl.h +++ b/src/include/libpq/sasl.h @@ -1,6 +1,10 @@ /*------------------------------------------------------------------------- * * sasl.h + * Defines the SASL mechanism interface for the libpq backend. Each SASL + * mechanism defines a frontend and a backend callback structure. + * + * See src/interfaces/libpq/fe-auth-sasl.h for the frontend counterpart. * * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California @@ -12,6 +16,7 @@ #ifndef PG_SASL_H #define PG_SASL_H +#include "lib/stringinfo.h" #include "libpq/libpq-be.h" /* Status codes for message exchange */ @@ -19,10 +24,107 @@ #define PG_SASL_EXCHANGE_SUCCESS 1 #define PG_SASL_EXCHANGE_FAILURE 2 -/* Backend mechanism API */ -typedef void (*pg_be_sasl_mechanism_func)(Port *, StringInfo); -typedef void *(*pg_be_sasl_init_func)(Port *, const char *, const char *); -typedef int (*pg_be_sasl_exchange_func)(void *, const char *, int, char **, int *, char **); +/* + * Maximum accepted size of SASL messages. + * + * The messages that the server or libpq generate are much smaller than this, + * but have some headroom. + */ +#define PG_MAX_SASL_MESSAGE_LENGTH 1024 + +/* + * Backend mechanism API + * + * To implement a backend mechanism, declare a pg_be_sasl_mech struct with + * appropriate callback implementations. Then pass the mechanism to + * CheckSASLAuth() during ClientAuthentication(), once the server has decided + * which authentication method to use. + */ + +/* + * mech.get_mechanisms() + * + * Retrieves the list of SASL mechanism names supported by this implementation. + * The names are appended into the provided buffer. + * + * Input parameters: + * + * port: the client Port + * + * Output parameters: + * + * buf: a StringInfo buffer that the callback should populate with supported + * mechanism names. Null-terminated names should be printed to the buffer + * using appendStringInfo*(). + */ +typedef void (*pg_be_sasl_mechanism_func)(Port *port, StringInfo buf); + +/* + * mech.init() + * + * Initializes mechanism-specific state for a connection. This callback must + * return a pointer to its allocated state, which will be passed as-is as the + * first argument to the other callbacks. + * + * Input paramters: + * + * port: the client Port + * + * mech: the actual mechanism name in use by the client + * + * shadow_pass: the shadow entry for the user being authenticated, or NULL if + * one does not exist. Mechanisms that do not use shadow entries + * may ignore this parameter. If a mechanism uses shadow entries + * but shadow_pass is NULL, the implementation must continue the + * exchange as if the user existed and the password did not + * match, to avoid disclosing valid user names. + */ +typedef void *(*pg_be_sasl_init_func)(Port *port, const char *mech, + const char *shadow_pass); + +/* + * mech.exchange() + * + * Produces a server challenge to be sent to the client. The callback must + * return one of the PG_SASL_EXCHANGE_* values, depending on whether the + * exchange must continue, has finished successfully, or has failed. + * + * Input parameters: + * + * state: the opaque mechanism state returned by mech.init() + * + * input: the response data sent by the client, or NULL if the mechanism is + * client-first but the client did not send an initial response. + * (This can only happen during the first message from the client.) + * This is guaranteed to be null-terminated for safety, but SASL + * allows embedded nulls in responses, so mechanisms must be careful + * to check inputlen. + * + * inputlen: the length of the challenge data sent by the server, or -1 if the + * client did not send an initial response + * + * Output parameters, to be set by the callback function: + * + * output: a palloc'd buffer containing either the server's next challenge + * (if PG_SASL_EXCHANGE_CONTINUE is returned) or the server's + * outcome data (if PG_SASL_EXCHANGE_SUCCESS is returned and the + * mechanism requires data to be sent during a successful outcome). + * The callback should set this to NULL if the exchange is over and + * no output should be sent, which should correspond to either + * PG_SASL_EXCHANGE_FAILURE or a PG_SASL_EXCHANGE_SUCCESS with no + * outcome data. + * + * outputlen: the length of the challenge data. Ignored if *output is NULL. + * + * logdetail: set to an optional DETAIL message to be printed to the server + * log, to disambiguate failure modes. (The client will only ever + * see the same generic authentication failure message.) Ignored if + * the exchange is completed with PG_SASL_EXCHANGE_SUCCESS. + */ +typedef int (*pg_be_sasl_exchange_func)(void *state, + const char *input, int inputlen, + char **output, int *outputlen, + char **logdetail); typedef struct { @@ -31,4 +133,8 @@ typedef struct pg_be_sasl_exchange_func exchange; } pg_be_sasl_mech; +/* Common implementation for auth.c */ +extern int CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, + char *shadow_pass, char **logdetail); + #endif /* PG_SASL_H */ diff --git a/src/interfaces/libpq/fe-auth-sasl.h b/src/interfaces/libpq/fe-auth-sasl.h new file mode 100644 index 0000000000..1409e51287 --- /dev/null +++ b/src/interfaces/libpq/fe-auth-sasl.h @@ -0,0 +1,131 @@ +/*------------------------------------------------------------------------- + * + * fe-auth-sasl.h + * Defines the SASL mechanism interface for the libpq frontend. Each SASL + * mechanism defines a frontend and a backend callback structure. This is not + * part of the public API for applications. + * + * See src/include/libpq/sasl.h for the backend counterpart. + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq/fe-auth-sasl.h + * + *------------------------------------------------------------------------- + */ + +#ifndef FE_AUTH_SASL_H +#define FE_AUTH_SASL_H + +#include "libpq-fe.h" + +/* + * Frontend mechanism API + * + * To implement a frontend mechanism, declare a pg_be_sasl_mech struct with + * appropriate callback implementations, then hook it into conn->sasl during + * pg_SASL_init()'s mechanism negotiation. + */ + +/* + * mech.init() + * + * Initializes mechanism-specific state for a connection. This callback must + * return a pointer to its allocated state, which will be passed as-is as the + * first argument to the other callbacks. mech.free() will be called to release + * any state resources. + * + * If state allocation fails, the implementation should return NULL to fail the + * authentication exchange. + * + * Input parameters: + * + * conn: the connection to the server + * + * password: the user's supplied password for the current connection + * + * mech: the mechanism name in use, for implementations that may advertise + * more than one name (such as *-PLUS variants) + */ +typedef void *(*pg_fe_sasl_init_func)(PGconn *conn, const char *password, + const char *mech); + +/* + * mech.exchange() + * + * Produces a client response to a server challenge. As a special case for + * client-first SASL mechanisms, exchange() is called with a NULL server + * response once at the start of the authentication exchange to generate an + * initial response. + * + * Input parameters: + * + * state: the opaque mechanism state returned by mech.init() + * + * input: the challenge data sent by the server, or NULL when generating a + * client-first initial response (that is, when the server expects + * the client to send a message to start the exchange). This is + * guaranteed to be null-terminated for safety, but SASL allows + * embedded nulls in challenges, so mechanisms must be careful to + * check inputlen. + * + * inputlen: the length of the challenge data sent by the server, or -1 + * during client-first initial response generation. + * + * Output parameters, to be set by the callback function: + * + * output: a malloc'd buffer containing the client's response to the + * server, or NULL if the exchange should be aborted. (*success + * should be set to false in the latter case.) + * + * outputlen: the length of the client response buffer, or zero if no data + * should be sent due to an exchange failure + * + * done: set to true if the SASL exchange should not continue, because + * the exchange is either complete or failed + * + * success: set to true if the SASL exchange completed successfully. Ignored + * if *done is false. + */ +typedef void (*pg_fe_sasl_exchange_func)(void *state, + char *input, int inputlen, + char **output, int *outputlen, + bool *done, bool *success); + +/* + * mech.channel_bound() + * + * Returns true if the connection has an established channel binding. A + * mechanism implementation must ensure that a SASL exchange has actually been + * completed, in addition to checking that channel binding is in use. + * + * Mechanisms that do not implement channel binding may simply return false. + * + * Input parameters: + * + * state: the opaque mechanism state returned by mech.init() + */ +typedef bool (*pg_fe_sasl_channel_bound_func)(void *); + +/* + * mech.free() + * + * Frees the state allocated by mech.init(). This is called when the connection + * is dropped, not when the exchange is completed. + * + * Input parameters: + * + * state: the opaque mechanism state returned by mech.init() + */ +typedef void (*pg_fe_sasl_free_func)(void *); + +typedef struct +{ + pg_fe_sasl_init_func init; + pg_fe_sasl_exchange_func exchange; + pg_fe_sasl_channel_bound_func channel_bound; + pg_fe_sasl_free_func free; +} pg_fe_sasl_mech; + +#endif /* FE_AUTH_SASL_H */ diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index d5cbac108e..f299e72e7e 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -41,6 +41,7 @@ #include "common/md5.h" #include "common/scram-common.h" #include "fe-auth.h" +#include "fe-auth-sasl.h" #include "libpq-fe.h" #ifdef ENABLE_GSS @@ -672,6 +673,11 @@ pg_SASL_continue(PGconn *conn, int payloadlen, bool final) libpq_gettext("AuthenticationSASLFinal received from server, but SASL authentication was not completed\n")); return STATUS_ERROR; } + /* + * TODO SASL requires us to accomodate zero-length responses. + * TODO is it legal for a client not to send a response to a server + * challenge, if the exchange isn't being aborted? + */ if (outputlen != 0) { /* diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 3ebf111158..e9f214b61b 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -41,6 +41,7 @@ #include "getaddrinfo.h" #include "libpq/pqcomm.h" /* include stuff found in fe only */ +#include "fe-auth-sasl.h" #include "pqexpbuffer.h" #ifdef ENABLE_GSS @@ -339,19 +340,6 @@ typedef struct pg_conn_host * found in password file. */ } pg_conn_host; -typedef void *(*pg_fe_sasl_init_func)(PGconn *, const char *, const char *); -typedef void (*pg_fe_sasl_exchange_func)(void *, char *, int, char **, int *, bool *, bool *); -typedef bool (*pg_fe_sasl_channel_bound_func)(void *); -typedef void (*pg_fe_sasl_free_func)(void *); - -typedef struct -{ - pg_fe_sasl_init_func init; - pg_fe_sasl_exchange_func exchange; - pg_fe_sasl_channel_bound_func channel_bound; - pg_fe_sasl_free_func free; -} pg_fe_sasl_mech; - /* * PGconn stores all the state data associated with a single connection * to a backend.