0007-Support-for-SCRAM-SHA-256-authentication-RFC-5802-an.patch
application/x-download
Filename: 0007-Support-for-SCRAM-SHA-256-authentication-RFC-5802-an.patch
Type: application/x-download
Part: 6
Patch
Format: format-patch
Series: patch 0007
Subject: Support for SCRAM-SHA-256 authentication (RFC 5802 and 7677)
| File | + | − |
|---|---|---|
| contrib/passwordcheck/passwordcheck.c | 16 | 3 |
| doc/src/sgml/catalogs.sgml | 12 | 7 |
| doc/src/sgml/config.sgml | 8 | 5 |
| doc/src/sgml/protocol.sgml | 142 | 5 |
| doc/src/sgml/ref/create_role.sgml | 12 | 11 |
| src/backend/commands/user.c | 40 | 9 |
| src/backend/libpq/auth.c | 111 | 0 |
| src/backend/libpq/auth-scram.c | 956 | 0 |
| src/backend/libpq/crypt.c | 49 | 20 |
| src/backend/libpq/hba.c | 13 | 0 |
| src/backend/libpq/Makefile | 1 | 1 |
| src/backend/libpq/pg_hba.conf.sample | 4 | 4 |
| src/backend/utils/misc/guc.c | 1 | 0 |
| src/backend/utils/misc/postgresql.conf.sample | 1 | 1 |
| src/common/Makefile | 1 | 1 |
| src/common/scram-common.c | 195 | 0 |
| src/include/commands/user.h | 2 | 1 |
| src/include/common/scram-common.h | 56 | 0 |
| src/include/libpq/crypt.h | 8 | 0 |
| src/include/libpq/hba.h | 1 | 0 |
| src/include/libpq/libpq-be.h | 3 | 1 |
| src/include/libpq/pqcomm.h | 2 | 0 |
| src/include/libpq/scram.h | 35 | 0 |
| src/interfaces/libpq/fe-auth.c | 108 | 0 |
| src/interfaces/libpq/fe-auth.h | 8 | 0 |
| src/interfaces/libpq/fe-auth-scram.c | 544 | 0 |
| src/interfaces/libpq/fe-connect.c | 52 | 0 |
| src/interfaces/libpq/.gitignore | 5 | 0 |
| src/interfaces/libpq/libpq-int.h | 5 | 0 |
| src/interfaces/libpq/Makefile | 11 | 6 |
| src/tools/msvc/Mkvcbuild.pm | 8 | 2 |
From e106c68e34ac74d21020441822b7d6bdc20c4beb Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 20 Oct 2016 14:03:23 +0900
Subject: [PATCH 7/8] Support for SCRAM-SHA-256 authentication (RFC 5802 and
7677)
SHA-256 is used as hashing function. This commit introduces the basic
SASL communication protocol plugged in on top of the existing
infrastructure.
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 | 23 +-
src/backend/commands/user.c | 49 +-
src/backend/libpq/Makefile | 2 +-
src/backend/libpq/auth-scram.c | 956 ++++++++++++++++++++++++++
src/backend/libpq/auth.c | 111 +++
src/backend/libpq/crypt.c | 69 +-
src/backend/libpq/hba.c | 13 +
src/backend/libpq/pg_hba.conf.sample | 8 +-
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 | 56 ++
src/include/libpq/crypt.h | 8 +
src/include/libpq/hba.h | 1 +
src/include/libpq/libpq-be.h | 4 +-
src/include/libpq/pqcomm.h | 2 +
src/include/libpq/scram.h | 35 +
src/interfaces/libpq/.gitignore | 5 +
src/interfaces/libpq/Makefile | 17 +-
src/interfaces/libpq/fe-auth-scram.c | 544 +++++++++++++++
src/interfaces/libpq/fe-auth.c | 108 +++
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 +-
31 files changed, 2410 insertions(+), 77 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 @@
<entry><type>text</type></entry>
<entry>
Password (possibly encrypted); null if none. If the password
- is encrypted, this column will begin with the string <literal>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 <literal>joe</> has password <literal>xyzzy</>,
- <productname>PostgreSQL</> will store the md5 hash of
- <literal>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
+ <literal>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 <literal>joe</> has password
+ <literal>xyzzy</>, <productname>PostgreSQL</> will store the md5
+ hash of <literal>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.
</entry>
</row>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 99ff9f5..5ed9ef5 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 <literal>md5</>, which
stores the password as an MD5 hash. Setting this to <literal>plain</> stores
it in plaintext. <literal>on</> and <literal>off</> are also accepted, as
- aliases for <literal>md5</> and <literal>plain</>, respectively.
- </para>
-
+ aliases for <literal>md5</> and <literal>plain</>, respectively. Setting
+ this parameter to <literal>scram</> will encrypt the password with
+ SCRAM-SHA-256.
+ </para>
</listitem>
</varlistentry>
@@ -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
- <literal>md5</> uses the user name as salt on both the
- client and server, <literal>md5</> cannot be used with
+ <literal>md5</>uses the user name as salt on both the
+ client and server, and <literal>scram</> uses the user name as
+ a portion of the salt used on both the client and server,
+ <literal>md5</> and <literal>scram</> cannot be used with
<varname>db_user_namespace</>.
</para>
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.
</para>
<para>
@@ -366,6 +366,35 @@
</listitem>
</varlistentry>
+ <varlistentry>
+ <term>AuthenticationSASL</term>
+ <listitem>
+ <para>
+ 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.
+ </para>
+ </listitem>
+
+ </varlistentry>
+ <varlistentry>
+ <term>AuthenticationSASLContinue</term>
+ <listitem>
+ <para>
+ 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.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</para>
@@ -2585,6 +2614,114 @@ AuthenticationGSSContinue (B)
</listitem>
</varlistentry>
+<varlistentry>
+<term>
+AuthenticationSASL (B)
+</term>
+<listitem>
+<para>
+
+<variablelist>
+<varlistentry>
+<term>
+ Byte1('R')
+</term>
+<listitem>
+<para>
+ Identifies the message as an authentication request.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+ Int32
+</term>
+<listitem>
+<para>
+ Length of message contents in bytes, including self.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+ Int32(10)
+</term>
+<listitem>
+<para>
+ Specifies that SASL authentication is started.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+ String
+</term>
+<listitem>
+<para>
+ Name of a SASL authentication mechanism.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+
+</para>
+</listitem>
+</varlistentry>
+
+<varlistentry>
+<term>
+AuthenticationSASLContinue (B)
+</term>
+<listitem>
+<para>
+
+<variablelist>
+<varlistentry>
+<term>
+ Byte1('R')
+</term>
+<listitem>
+<para>
+ Identifies the message as an authentication request.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+ Int32
+</term>
+<listitem>
+<para>
+ Length of message contents in bytes, including self.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+ Int32(11)
+</term>
+<listitem>
+<para>
+ Specifies that this message contains SASL-mechanism specific
+ data.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+ Byte<replaceable>n</replaceable>
+</term>
+<listitem>
+<para>
+ SASL data, specific to the SASL mechanism being used.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+
+</para>
+</listitem>
+</varlistentry>
<varlistentry>
<term>
@@ -4347,7 +4484,7 @@ PasswordMessage (F)
<listitem>
<para>
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 d172ff1..e19bc8c 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -229,16 +229,16 @@ CREATE ROLE <replaceable class="PARAMETER">name</replaceable> [ [ WITH ] <replac
encrypted in the system catalogs. (If neither is specified,
the default behavior is determined by the configuration
parameter <xref linkend="guc-password-encryption">.) If the
- presented password string is already in MD5-encrypted format,
- then it is stored encrypted as-is, regardless of whether
- <literal>ENCRYPTED</> or <literal>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 <literal>ENCRYPTED</> or <literal>UNENCRYPTED</>
+ is specified (since the system cannot decrypt the specified encrypted
+ password string). This allows reloading of encrypted passwords
+ during dump/restore.
</para>
<para>
- 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.
</para>
@@ -254,10 +254,11 @@ CREATE ROLE <replaceable class="PARAMETER">name</replaceable> [ [ WITH ] <replac
attribute, but you can nonetheless define one for roles without it.)
If you do not plan to use password authentication you can omit this
option. The protocols supported are <literal>md5</> to enforce
- a password to be MD5-encrypted, and <literal>plain</> to use an
- unencrypted password. If the password string is already in
- MD5-encrypted format, then it is stored encrypted even if
- <literal>plain</> is specified.
+ a password to be MD5-encrypted, <literal>scram</> to enforce a password
+ to be encrypted with SCRAM-SHA-256, and <literal>plain</> to use
+ an unencrypted password. If the password string is already in
+ MD5-encrypted or SCRAM-SHA-256 format, then it is stored encrypted
+ even if another protocol is specified.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 25f9431..7e03ac4 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 */
}
@@ -221,6 +225,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
password_type = PASSWORD_TYPE_MD5;
else if (strcmp(protocol, "plain") == 0)
password_type = PASSWORD_TYPE_PLAINTEXT;
+ else if (strcmp(protocol, "scram") == 0)
+ password_type = PASSWORD_TYPE_SCRAM;
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -450,11 +456,22 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
* Call the password checking hook if there is one defined
*/
if (check_password_hook && password)
+ {
+ int type;
+
+ if (isMD5(password))
+ type = PASSWORD_TYPE_MD5;
+ else if (is_scram_verifier(password))
+ type = PASSWORD_TYPE_SCRAM;
+ else
+ type = PASSWORD_TYPE_PLAINTEXT;
+
(*check_password_hook) (stmt->role,
password,
- isMD5(password) ? PASSWORD_TYPE_MD5 : PASSWORD_TYPE_PLAINTEXT,
+ type,
validUntil_datum,
validUntil_null);
+ }
/*
* Build a tuple to insert
@@ -663,6 +680,8 @@ AlterRole(AlterRoleStmt *stmt)
password_type = PASSWORD_TYPE_MD5;
else if (strcmp(protocol, "plain") == 0)
password_type = PASSWORD_TYPE_PLAINTEXT;
+ else if (strcmp(protocol, "scram") == 0)
+ password_type = PASSWORD_TYPE_SCRAM;
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -859,11 +878,23 @@ AlterRole(AlterRoleStmt *stmt)
* Call the password checking hook if there is one defined
*/
if (check_password_hook && password)
+ {
+ int type;
+
+ if (isMD5(password))
+ type = PASSWORD_TYPE_MD5;
+ else if (is_scram_verifier(password))
+ type = PASSWORD_TYPE_SCRAM;
+ else
+ type = PASSWORD_TYPE_PLAINTEXT;
+
(*check_password_hook) (rolename,
password,
- isMD5(password) ? PASSWORD_TYPE_MD5 : PASSWORD_TYPE_PLAINTEXT,
+ type,
validUntil_datum,
validUntil_null);
+ }
+
/*
* Build an updated tuple, perusing the information just obtained
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..b5ddfac
--- /dev/null
+++ b/src/backend/libpq/auth-scram.c
@@ -0,0 +1,956 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 <unistd.h>
+
+#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"
+#include "utils/timestamp.h"
+
+/*
+ * Status data for SCRAM. This should be kept internal to this file.
+ */
+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;
+
+/*
+ * Internal error codes for exchange functions.
+ * Callers of the exchange routines do not need to be aware of any of
+ * that and should just send messages generated here.
+ */
+typedef enum
+{
+ SASL_NO_ERROR = 0,
+ /* error codes */
+ SASL_INVALID_ENCODING,
+ SASL_EXTENSIONS_NOT_SUPPORTED,
+ SASL_CHANNEL_BINDING_UNMATCH,
+ SASL_CHANNEL_BINDING_NO_SUPPORT,
+ SASL_CHANNEL_BINDING_TYPE_NOT_SUPPORTED,
+ SASL_UNKNOWN_USER,
+ SASL_INVALID_PROOF,
+ SASL_INVALID_USERNAME_ENCODING,
+ SASL_NO_RESOURCES,
+ /*
+ * Unrecognized errors should be treated as "other-error". In order to
+ * prevent information disclosure, the server may substitute the real
+ * reason with "other-error".
+ */
+ SASL_OTHER_ERROR
+} SASLStatus;
+
+
+static SASLStatus read_client_first_message(scram_state *state,
+ char *input, char **logdetail);
+static SASLStatus read_client_final_message(scram_state *state,
+ char *input, char **logdetail);
+static char *build_server_first_message(scram_state *state);
+static char *build_server_final_message(scram_state *state);
+static char *build_error_message(SASLStatus status);
+static SASLStatus check_client_data(void *opaque, char **logdetail);
+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);
+
+/*
+ * build_error_message
+ *
+ * Build an error message for a problem that happened during the SASL
+ * message exchange. Those messages are formatted with e= as prefix
+ * and need to be sent back to the client.
+ */
+static char *
+build_error_message(SASLStatus status)
+{
+ char *res = NULL;
+
+ /*
+ * The following error format is respected here:
+ *
+ * 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.
+ */
+
+ switch (status)
+ {
+ case SASL_INVALID_ENCODING:
+ res = psprintf("e=invalid-encoding");
+ break;
+ case SASL_EXTENSIONS_NOT_SUPPORTED:
+ res = psprintf("e=extensions-not-supported");
+ break;
+ case SASL_INVALID_PROOF:
+ res = psprintf("e=invalid-proof");
+ break;
+ case SASL_CHANNEL_BINDING_UNMATCH:
+ res = psprintf("e=channel-bindings-dont-match");
+ break;
+ case SASL_CHANNEL_BINDING_NO_SUPPORT:
+ res = psprintf("e=server-does-support-channel-binding");
+ break;
+ case SASL_CHANNEL_BINDING_TYPE_NOT_SUPPORTED:
+ res = psprintf("e=unsupported-channel-binding-type");
+ break;
+ case SASL_UNKNOWN_USER:
+ res = psprintf("e=unknown-user");
+ break;
+ case SASL_INVALID_USERNAME_ENCODING:
+ res = psprintf("e=invalid-username-encoding");
+ break;
+ case SASL_NO_RESOURCES:
+ res = psprintf("e=no-resources");
+ break;
+ case SASL_OTHER_ERROR:
+ res = psprintf("e=other-error");
+ break;
+ case SASL_NO_ERROR:
+ default:
+ Assert(0); /* should not happen */
+ }
+
+ Assert(res != NULL);
+ return res;
+}
+
+/*
+ * pg_be_scram_init
+ *
+ * Initialize a new SCRAM authentication exchange status tracker. This
+ * needs to be called before doing any exchange. It will be filled later
+ * after the beginning of the exchange with verifier data.
+ */
+void *
+pg_be_scram_init(char *username)
+{
+ scram_state *state;
+
+ state = (scram_state *) palloc0(sizeof(scram_state));
+ state->state = INIT;
+ state->username = username;
+ state->salt = NULL;
+
+ return state;
+}
+
+/*
+ * check_client_data
+ *
+ * Fill into the SASL exchange status all the information related to user and
+ * perform sanity checks.
+ */
+static SASLStatus
+check_client_data(void *opaque, char **logdetail)
+{
+ scram_state *state = (scram_state *) opaque;
+ char *server_key;
+ char *stored_key;
+ char *salt;
+ int iterations;
+ int res;
+ char *passwd;
+ TimestampTz vuntil = 0;
+ bool vuntil_null;
+
+ /* compute the salt to use for computing responses */
+ if (!pg_strong_random(MyProcPort->SASLSalt, sizeof(MyProcPort->SASLSalt)))
+ {
+ *logdetail = psprintf(_("Could not generate random salt"));
+ return SASL_OTHER_ERROR;
+ }
+
+ /*
+ * Fetch details about role needed for password checks.
+ */
+ res = get_role_details(state->username, &passwd, &vuntil,
+ &vuntil_null,
+ logdetail);
+
+ /*
+ * Check if an error has happened. "logdetail" is already filled,
+ * here we just need to find what is the error mapping with the SASL
+ * exchange and let the client know what happened.
+ */
+ if (res == PG_ROLE_NOT_DEFINED)
+ return SASL_UNKNOWN_USER;
+ else if (res == PG_ROLE_NO_PASSWORD ||
+ res == PG_ROLE_EMPTY_PASSWORD)
+ return SASL_OTHER_ERROR;
+
+ /*
+ * Check password validity, there is nothing to do if the password
+ * validity field is null.
+ */
+ if (!vuntil_null)
+ {
+ if (vuntil < GetCurrentTimestamp())
+ {
+ *logdetail = psprintf(_("User \"%s\" has an expired password."),
+ state->username);
+ pfree(passwd);
+ return SASL_OTHER_ERROR;
+ }
+ }
+
+ /* The SCRAM verifier needs to be in correct shape as well. */
+ if (!parse_scram_verifier(passwd, &salt, &iterations,
+ &stored_key, &server_key))
+ {
+ *logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."),
+ state->username);
+ pfree(passwd);
+ return SASL_OTHER_ERROR;
+ }
+
+ /* OK to fill in everything */
+ 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);
+ pfree(passwd);
+ return SASL_NO_ERROR;
+}
+
+/*
+ * Continue a SCRAM authentication exchange.
+ *
+ * The next message to send to client is saved in "output", for a length
+ * of "outputlen". In the case of an error, optionally store a palloc'd
+ * string at *logdetail that will be sent to the postmaster log (but not
+ * the client).
+ */
+int
+pg_be_scram_exchange(void *opaq, char *input, int inputlen,
+ char **output, int *outputlen, char **logdetail)
+{
+ scram_state *state = (scram_state *) opaq;
+ SASLStatus status;
+ int result;
+
+ *output = NULL;
+ *outputlen = 0;
+
+ if (inputlen > 0)
+ elog(DEBUG4, "got SCRAM message: %s", input);
+
+ switch (state->state)
+ {
+ case INIT:
+ /*
+ * Initialization phase. Things happen in the following order:
+ * 1) Receive the first message from client and be sure that it
+ * is parsed correctly.
+ * 2) Validate the user information. A couple of things are done
+ * here, mainly validity checks on the password and the user.
+ * 3) Send the challenge to the client.
+ */
+ status = read_client_first_message(state, input, logdetail);
+ if (status != SASL_NO_ERROR)
+ {
+ *output = build_error_message(status);
+ *outputlen = strlen(*output);
+ result = SASL_EXCHANGE_FAILURE;
+ break;
+ }
+
+ /* check validity of user data */
+ status = check_client_data(state, logdetail);
+ if (status != SASL_NO_ERROR)
+ {
+ *output = build_error_message(status);
+ *outputlen = strlen(*output);
+ result = SASL_EXCHANGE_FAILURE;
+ break;
+ }
+
+ /* prepare message to send challenge */
+ *output = build_server_first_message(state);
+ if (*output == NULL)
+ {
+ *output = build_error_message(SASL_OTHER_ERROR);
+ *outputlen = strlen(*output);
+ result = SASL_EXCHANGE_FAILURE;
+ break;
+ }
+ *outputlen = strlen(*output);
+ state->state = SALT_SENT;
+ result = SASL_EXCHANGE_CONTINUE;
+ break;
+
+ case SALT_SENT:
+ /*
+ * Final phase for the server. First receive the response to
+ * the challenge previously sent and then let the client know
+ * that everything went well.
+ */
+ status = read_client_final_message(state, input, logdetail);
+ if (status != SASL_NO_ERROR)
+ {
+ *output = build_error_message(status);
+ *outputlen = strlen(*output);
+ result = SASL_EXCHANGE_FAILURE;
+ break;
+ }
+
+ /* Now check the final nonce and the client proof */
+ if (!verify_final_nonce(state) ||
+ !verify_client_proof(state))
+ {
+ *output = build_error_message(SASL_INVALID_PROOF);
+ *outputlen = strlen(*output);
+ result = SASL_EXCHANGE_FAILURE;
+ break;
+ }
+
+ /* Build final message for client */
+ *output = build_server_final_message(state);
+ if (*output == NULL)
+ {
+ *output = build_error_message(SASL_OTHER_ERROR);
+ *outputlen = strlen(*output);
+ result = SASL_EXCHANGE_FAILURE;
+ break;
+ }
+
+ /* Success! */
+ *outputlen = strlen(*output);
+ result = SASL_EXCHANGE_SUCCESS;
+ 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(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.
+ *
+ * Returns true if the SCRAM verifier has been parsed, and false otherwise.
+ */
+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, SCRAM_ITERATION_LEN);
+ 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')))
+ return NULL;
+ if (attr_p)
+ *attr_p = attr;
+ begin++;
+
+ if (*begin != '=')
+ return NULL;
+ 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. In the event of an error, returns
+ * to caller a e= message to be used for the rest of the exchange, or
+ * NULL in case of success.
+ */
+static SASLStatus
+read_client_first_message(scram_state *state, char *input, char **logdetail)
+{
+ input = pstrdup(input);
+
+ /*
+ * saslname = 1*(value-safe-char / "=2C" / "=3D")
+ * ;; Conforms to <value>.
+ *
+ * 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 */
+ *logdetail = psprintf(_("channel binding not supported."));
+ return SASL_CHANNEL_BINDING_NO_SUPPORT;
+ }
+
+ /* any mandatory extensions would go here. */
+ if (*input != ',')
+ {
+ *logdetail = psprintf(_("mandatory extension %c not supported."), *input);
+ return SASL_OTHER_ERROR;
+ }
+ 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! */
+ return SASL_EXCHANGE_CONTINUE;
+}
+
+/*
+ * 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(SCRAM_NONCE_LEN) + 1);
+ encoded_len = pg_b64_encode(nonce, SCRAM_NONCE_LEN, state->server_nonce);
+
+ if (encoded_len < 0)
+ return NULL;
+
+ 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 SASLStatus
+read_client_final_message(scram_state *state, char *input, char **logdetail)
+{
+ 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)
+ {
+ *logdetail = psprintf(_("invalid channel binding input."));
+ return SASL_CHANNEL_BINDING_TYPE_NOT_SUPPORTED;
+ }
+ 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(strlen(value)));
+ if (pg_b64_decode(value, strlen(value), client_proof) != SCRAM_KEY_LEN)
+ {
+ *logdetail = psprintf(_("invalid client proof."));
+ return SASL_INVALID_PROOF;
+ }
+ memcpy(state->ClientProof, client_proof, SCRAM_KEY_LEN);
+ pfree(client_proof);
+
+ if (*p != '\0')
+ {
+ *logdetail = psprintf(_("malformed SCRAM message (garbage at end of message %c)."),
+ attr);
+ return SASL_OTHER_ERROR;
+ }
+
+ 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 */
+ return SASL_NO_ERROR;
+}
+
+/*
+ * 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(SCRAM_KEY_LEN) + 1);
+ siglen = pg_b64_encode((const char *) ServerSignature,
+ SCRAM_KEY_LEN, server_signature_base64);
+ if (siglen < 0)
+ return NULL;
+ server_signature_base64[siglen] = '\0';
+
+ /*
+ * The following error is generated:
+ *
+ * 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 44b2212..6f774d9 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -30,6 +30,7 @@
#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"
@@ -210,6 +211,12 @@ static int CheckRADIUSAuth(Port *port);
/*----------------------------------------------------------------
+ * SASL authentication
+ *----------------------------------------------------------------
+ */
+static int CheckSASLAuth(Port *port, char **logdetail);
+
+/*----------------------------------------------------------------
* Global authentication functions
*----------------------------------------------------------------
*/
@@ -271,6 +278,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;
@@ -549,6 +557,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, "");
@@ -738,6 +750,105 @@ 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 *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.)
+ */
+ if (PG_PROTOCOL_MAJOR(FrontendProtocol) < 3)
+ ereport(FATAL,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("SASL authentication is not supported in protocol version 2")));
+
+ /*
+ * Send first the authentication request to user. As for MD5, we want
+ * the user to send its password first even if nothing has been done
+ * yet. This avoids consistency issues where a user would be able to
+ * guess that a server is expecting SASL or MD5 depending on the answer
+ * given by the backend without the user providing a password first.
+ */
+ sendAuthRequest(port, AUTH_REQ_SASL, SCRAM_SHA256_NAME,
+ strlen(SCRAM_SHA256_NAME) + 1);
+
+ /* Initialize the status tracker for message exchanges */
+ scram_opaq = pg_be_scram_init(port->user_name);
+
+ /*
+ * 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 = pg_be_scram_exchange(scram_opaq, buf.data, buf.len,
+ &output, &outputlen, logdetail);
+
+ /* 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);
+
+ /* Oops, Something bad happened */
+ if (result != SASL_EXCHANGE_SUCCESS)
+ {
+ /* an error should have been set during the exchange checks */
+ Assert(*logdetail != NULL);
+ return STATUS_ERROR;
+ }
+
+ return STATUS_OK;
+}
/*----------------------------------------------------------------
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index d84a180..1c9db9a 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -1,8 +1,8 @@
/*-------------------------------------------------------------------------
*
* crypt.c
- * Look into the password file and check the encrypted password with
- * the one passed in from the frontend.
+ * Set of routines to look into the password file and check the
+ * encrypted password with the one passed in from the frontend.
*
* Original coding by Todd A. Brandys
*
@@ -30,30 +30,31 @@
/*
- * Check given password for given user, and return STATUS_OK or STATUS_ERROR.
- * In the error case, optionally store a palloc'd string at *logdetail
- * that will be sent to the postmaster log (but not the client).
+ * Fetch information of a given role necessary to check password data,
+ * and returns status code describing the error. In the case of an error,
+ * store a palloc'd string at *logdetail that will be sent to the postmaster
+ * log (but not the client!).
*/
int
-md5_crypt_verify(const Port *port, const char *role, char *client_pass,
+get_role_details(const char *role,
+ char **password,
+ TimestampTz *vuntil,
+ bool *vuntil_null,
char **logdetail)
{
- int retval = STATUS_ERROR;
- char *shadow_pass,
- *crypt_pwd;
- TimestampTz vuntil = 0;
- char *crypt_client_pass = client_pass;
HeapTuple roleTup;
Datum datum;
bool isnull;
+ *vuntil = 0;
+ *vuntil_null = true;
+
/* Get role info from pg_authid */
roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
if (!HeapTupleIsValid(roleTup))
{
- *logdetail = psprintf(_("Role \"%s\" does not exist."),
- role);
- return STATUS_ERROR; /* no such user */
+ *logdetail = psprintf(_("Role \"%s\" does not exist."), role);
+ return PG_ROLE_NOT_DEFINED; /* no such user */
}
datum = SysCacheGetAttr(AUTHNAME, roleTup,
@@ -63,24 +64,52 @@ md5_crypt_verify(const Port *port, const char *role, char *client_pass,
ReleaseSysCache(roleTup);
*logdetail = psprintf(_("User \"%s\" has no password assigned."),
role);
- return STATUS_ERROR; /* user has no password */
+ return PG_ROLE_NO_PASSWORD; /* user has no password */
}
- shadow_pass = TextDatumGetCString(datum);
+ *password = TextDatumGetCString(datum);
datum = SysCacheGetAttr(AUTHNAME, roleTup,
Anum_pg_authid_rolvaliduntil, &isnull);
if (!isnull)
- vuntil = DatumGetTimestampTz(datum);
+ {
+ *vuntil = DatumGetTimestampTz(datum);
+ *vuntil_null = false;
+ }
ReleaseSysCache(roleTup);
- if (*shadow_pass == '\0')
+ if (**password == '\0')
{
*logdetail = psprintf(_("User \"%s\" has an empty password."),
role);
- return STATUS_ERROR; /* empty password */
+ pfree(*password);
+ return PG_ROLE_EMPTY_PASSWORD; /* empty password */
}
+ return PG_ROLE_OK;
+}
+
+/*
+ * Check given password for given user, and return STATUS_OK or STATUS_ERROR.
+ * In the error case, optionally store a palloc'd string at *logdetail
+ * that will be sent to the postmaster log (but not the client).
+ */
+int
+md5_crypt_verify(const Port *port, const char *role, char *client_pass,
+ char **logdetail)
+{
+ int retval = STATUS_ERROR;
+ char *shadow_pass,
+ *crypt_pwd;
+ TimestampTz vuntil;
+ char *crypt_client_pass = client_pass;
+ bool vuntil_null;
+
+ /* fetch details about role needed for password checks */
+ if (get_role_details(role, &shadow_pass, &vuntil, &vuntil_null,
+ logdetail) != PG_ROLE_OK)
+ return STATUS_ERROR;
+
/*
* Compare with the encrypted or plain password depending on the
* authentication method being used for this connection. (We do not
@@ -152,7 +181,7 @@ md5_crypt_verify(const Port *port, const char *role, char *client_pass,
/*
* Password OK, now check to be sure we are not past rolvaliduntil
*/
- if (isnull)
+ if (vuntil_null)
retval = STATUS_OK;
else if (vuntil < GetCurrentTimestamp())
{
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/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 65660c1..2cc4747 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 159ada3..e4f0bd3 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..e9028fb
--- /dev/null
+++ b/src/include/common/scram-common.h
@@ -0,0 +1,56 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 SCRAM_NONCE_LEN
+
+/* number of bytes used when sending iteration number during exchange */
+#define SCRAM_ITERATION_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/crypt.h b/src/include/libpq/crypt.h
index 5725bb4..522cfa2 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -15,6 +15,14 @@
#include "libpq/libpq-be.h"
+/* Detailed error codes for get_role_details() */
+#define PG_ROLE_OK 0
+#define PG_ROLE_NOT_DEFINED 1
+#define PG_ROLE_NO_PASSWORD 2
+#define PG_ROLE_EMPTY_PASSWORD 3
+
+extern int get_role_details(const char *role, char **password,
+ TimestampTz *vuntil, bool *vuntil_null, 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 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 9648422..bbcb2f7 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..58638eb
--- /dev/null
+++ b/src/include/libpq/scram.h
@@ -0,0 +1,35 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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"
+
+/* Status codes for message exchange */
+#define SASL_EXCHANGE_CONTINUE 0
+#define SASL_EXCHANGE_SUCCESS 1
+#define SASL_EXCHANGE_FAILURE 2
+
+/* Routines dedicated to authentication */
+extern void *pg_be_scram_init(char *username);
+extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
+ char **output, int *outputlen, char **logdetail);
+
+/* Routines to handle and check SCRAM-SHA-256 verifier */
+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..2224ada 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
+/pg_strong_random.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..460b0a1 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 pg_strong_random.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 pg_strong_random.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..5bd2fbf
--- /dev/null
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -0,0 +1,544 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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,
+ PQExpBuffer errormessage);
+static char *build_client_final_message(fe_scram_state *state,
+ PQExpBuffer errormessage);
+static bool verify_server_proof(fe_scram_state *state);
+static bool 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, errorMessage);
+ if (*output == NULL)
+ {
+ *done = true;
+ *success = false;
+ break;
+ }
+ *outputlen = strlen(*output);
+ *done = false;
+ state->state = NONCE_SENT;
+ break;
+
+ case NONCE_SENT:
+ /* receive salt and server nonce, send response */
+ if (!read_server_first_message(state, input, errorMessage))
+ {
+ *done = true;
+ *success = false;
+ break;
+ }
+ *output = build_client_final_message(state, errorMessage);
+ if (*output == NULL)
+ {
+ *done = true;
+ *success = false;
+ break;
+ }
+ *outputlen = strlen(*output);
+ *done = false;
+ state->state = PROOF_SENT;
+ break;
+
+ case PROOF_SENT:
+ /* receive server proof, and verify it */
+ if (!read_server_final_message(state, input, errorMessage))
+ {
+ *done = true;
+ *success = false;
+ break;
+ }
+ *success = verify_server_proof(state);
+ *done = true;
+ state->state = FINISHED;
+ break;
+
+ default:
+ /* shouldn't happen */
+ *done = true;
+ *success = false;
+ printfPQExpBuffer(errorMessage,
+ libpq_gettext("invalid SCRAM exchange state\n"));
+ }
+}
+
+/*
+ * Read value for an attribute part of a SASL message.
+ *
+ * This routine is able to handle error messages e= sent by the server
+ * during the exchange of SASL messages. Returns NULL in case of error,
+ * setting errorMessage as well.
+ */
+static char *
+read_attr_value(char **input, char attr, PQExpBuffer errorMessage)
+{
+ char *begin = *input;
+ char *end;
+
+ if (*begin == 'e')
+ {
+ /* Received an error */
+ begin++;
+ if (*begin != '=')
+ {
+ printfPQExpBuffer(errorMessage,
+ libpq_gettext("malformed SCRAM message (expected = in attr e)\n"));
+ return NULL;
+ }
+
+ begin++;
+ printfPQExpBuffer(errorMessage,
+ libpq_gettext("error received from server in SASL exchange: %s\n"),
+ begin);
+ return NULL;
+ }
+
+ if (*begin != attr)
+ {
+ printfPQExpBuffer(errorMessage,
+ libpq_gettext("malformed SCRAM message (%c expected)\n"),
+ attr);
+ return NULL;
+ }
+ begin++;
+
+ if (*begin != '=')
+ {
+ printfPQExpBuffer(errorMessage,
+ libpq_gettext("malformed SCRAM message (expected = in attr %c)\n"),
+ attr);
+ return NULL;
+ }
+ 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, PQExpBuffer errormessage)
+{
+ char nonce[SCRAM_NONCE_LEN + 1];
+ char *buf;
+ char msglen;
+
+ if (!generate_nonce(nonce, SCRAM_NONCE_LEN))
+ {
+ printfPQExpBuffer(errormessage, libpq_gettext("failed to generate nonce\n"));
+ return NULL;
+ }
+
+ /* Generate message */
+ msglen = 5 + strlen(state->username) + 3 + strlen(nonce);
+ buf = malloc(msglen + 1);
+ if (buf == NULL)
+ {
+ printfPQExpBuffer(errormessage, libpq_gettext("out of memory\n"));
+ return NULL;
+ }
+ 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)
+ {
+ printfPQExpBuffer(errormessage, libpq_gettext("out of memory\n"));
+ return NULL;
+ }
+
+ return buf;
+}
+
+/*
+ * Build the final exchange message sent from the client.
+ */
+static char *
+build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
+{
+ 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(SCRAM_KEY_LEN) > sizeof(client_proof_base64))
+ {
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("malformed client proof (%d found)\n"),
+ pg_b64_enc_len(SCRAM_KEY_LEN));
+ return NULL;
+ }
+
+ client_proof_len = pg_b64_encode((char *) client_proof,
+ SCRAM_KEY_LEN,
+ client_proof_base64);
+ if (client_proof_len < 0)
+ {
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("failure when encoding client proof\n"));
+ return NULL;
+ }
+ client_proof_base64[client_proof_len] = '\0';
+
+ state->client_final_message_without_proof =
+ strdup(client_final_message_without_proof);
+ if (state->client_final_message_without_proof == NULL)
+ {
+ printfPQExpBuffer(errormessage, libpq_gettext("out of memory\n"));
+ return NULL;
+ }
+
+ 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;
+ char *server_nonce;
+
+ state->server_first_message = strdup(input);
+ if (!state->server_first_message)
+ {
+ printfPQExpBuffer(errormessage, libpq_gettext("out of memory\n"));
+ return false;
+ }
+
+ /* parse the message */
+ server_nonce = read_attr_value(&input, 'r', errormessage);
+ if (server_nonce == NULL)
+ {
+ /* read_attr_value() has generated an error string */
+ return false;
+ }
+
+ state->server_nonce = strdup(server_nonce);
+ if (state->server_nonce == NULL)
+ {
+ printfPQExpBuffer(errormessage, libpq_gettext("out of memory\n"));
+ return false;
+ }
+
+ encoded_salt = read_attr_value(&input, 's', errormessage);
+ if (encoded_salt == NULL)
+ {
+ /* read_attr_value() has generated an error string */
+ return false;
+ }
+ state->salt = malloc(pg_b64_dec_len(strlen(encoded_salt)));
+ if (state->salt == NULL)
+ {
+ printfPQExpBuffer(errormessage, libpq_gettext("out of memory\n"));
+ return false;
+ }
+ state->saltlen = pg_b64_decode(encoded_salt,
+ strlen(encoded_salt),
+ state->salt);
+ if (state->saltlen != SCRAM_SALT_LEN)
+ {
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("invalid salt length: found %d, expected %d\n"),
+ state->saltlen, SCRAM_SALT_LEN);
+ return false;
+ }
+
+ iterations_str = read_attr_value(&input, 'i', errormessage);
+ if (iterations_str == NULL || *input != '\0')
+ {
+ /* read_attr_value() has generated an error string */
+ return false;
+ }
+ state->iterations = strtol(iterations_str, &endptr, SCRAM_ITERATION_LEN);
+ if (*endptr != '\0')
+ {
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("malformed SCRAM message for number of iterations\n"));
+ 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)
+ {
+ printfPQExpBuffer(errormessage, libpq_gettext("out of memory\n"));
+ return false;
+ }
+
+ /* parse the message */
+ encoded_server_proof = read_attr_value(&input, 'v', errormessage);
+ if (encoded_server_proof == NULL || *input != '\0')
+ {
+ /* read_attr_value() has generated an error message */
+ 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,
+ libpq_gettext("invalid server proof\n"));
+ 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.
+ * Returns true of nonce has been succesfully generated, and false
+ * otherwise.
+ */
+static bool
+generate_nonce(char *buf, int len)
+{
+ if (!pg_strong_random(buf, len))
+ return false;
+
+ buf[len] = '\0';
+ return true;
+}
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 404bc93..809f0d7 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,35 @@ 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)
+ {
+ /* Use error message already if any set */
+ if (conn->errorMessage.len == 0)
+ 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 c5b737a..0f19d1c 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