postgres_fdw: SCRAM authentication pass-through
authorPeter Eisentraut <peter@eisentraut.org>
Wed, 15 Jan 2025 16:55:18 +0000 (17:55 +0100)
committerPeter Eisentraut <peter@eisentraut.org>
Wed, 15 Jan 2025 16:58:05 +0000 (17:58 +0100)
This enables SCRAM authentication for postgres_fdw when connecting to
a foreign server without having to store a plain-text password on user
mapping options.

This is done by saving the SCRAM ClientKey and ServeryKey from the
client authentication and using those instead of the plain-text
password for the server-side SCRAM exchange.  The new foreign-server
or user-mapping option "use_scram_passthrough" enables this.

Co-authored-by: Matheus Alcantara <mths.dev@pm.me>
Co-authored-by: Peter Eisentraut <peter@eisentraut.org>
Discussion: https://www.postgresql.org/message-id/flat/27b29a35-9b96-46a9-bc1a-914140869dac@gmail.com

14 files changed:
contrib/postgres_fdw/Makefile
contrib/postgres_fdw/connection.c
contrib/postgres_fdw/expected/postgres_fdw.out
contrib/postgres_fdw/meson.build
contrib/postgres_fdw/option.c
contrib/postgres_fdw/t/001_auth_scram.pl [new file with mode: 0644]
doc/src/sgml/libpq.sgml
doc/src/sgml/postgres-fdw.sgml
src/backend/libpq/auth-scram.c
src/include/libpq/libpq-be.h
src/interfaces/libpq/fe-auth-scram.c
src/interfaces/libpq/fe-auth.c
src/interfaces/libpq/fe-connect.c
src/interfaces/libpq/libpq-int.h

index 88fdce40d6a3a0ee1f20e8c17fe26f4e93a1cbee..adfbd2ef758e014a316c3f41119630820b72bed1 100644 (file)
@@ -17,6 +17,7 @@ EXTENSION = postgres_fdw
 DATA = postgres_fdw--1.0.sql postgres_fdw--1.0--1.1.sql postgres_fdw--1.1--1.2.sql
 
 REGRESS = postgres_fdw query_cancel
+TAP_TESTS = 1
 
 ifdef USE_PGXS
 PG_CONFIG = pg_config
index 202e7e583b343c9f8ebfe98d3ee90651511dc628..0274d6c253da2c91ca165a96c8cc7c1b0def7283 100644 (file)
@@ -19,6 +19,7 @@
 #include "access/xact.h"
 #include "catalog/pg_user_mapping.h"
 #include "commands/defrem.h"
+#include "common/base64.h"
 #include "funcapi.h"
 #include "libpq/libpq-be.h"
 #include "libpq/libpq-be-fe-helpers.h"
@@ -177,6 +178,7 @@ static void pgfdw_finish_abort_cleanup(List *pending_entries,
 static void pgfdw_security_check(const char **keywords, const char **values,
                                 UserMapping *user, PGconn *conn);
 static bool UserMappingPasswordRequired(UserMapping *user);
+static bool UseScramPassthrough(ForeignServer *server, UserMapping *user);
 static bool disconnect_cached_connections(Oid serverid);
 static void postgres_fdw_get_connections_internal(FunctionCallInfo fcinfo,
                                                  enum pgfdwVersion api_version);
@@ -485,7 +487,7 @@ connect_pg_server(ForeignServer *server, UserMapping *user)
         * for application_name, fallback_application_name, client_encoding,
         * end marker.
         */
-       n = list_length(server->options) + list_length(user->options) + 4;
+       n = list_length(server->options) + list_length(user->options) + 4 + 2;
        keywords = (const char **) palloc(n * sizeof(char *));
        values = (const char **) palloc(n * sizeof(char *));
 
@@ -554,10 +556,37 @@ connect_pg_server(ForeignServer *server, UserMapping *user)
        values[n] = GetDatabaseEncodingName();
        n++;
 
+       if (MyProcPort->has_scram_keys && UseScramPassthrough(server, user))
+       {
+           int         len;
+
+           keywords[n] = "scram_client_key";
+           len = pg_b64_enc_len(sizeof(MyProcPort->scram_ClientKey));
+           /* don't forget the zero-terminator */
+           values[n] = palloc0(len + 1);
+           pg_b64_encode((const char *) MyProcPort->scram_ClientKey,
+                         sizeof(MyProcPort->scram_ClientKey),
+                         (char *) values[n], len);
+           n++;
+
+           keywords[n] = "scram_server_key";
+           len = pg_b64_enc_len(sizeof(MyProcPort->scram_ServerKey));
+           /* don't forget the zero-terminator */
+           values[n] = palloc0(len + 1);
+           pg_b64_encode((const char *) MyProcPort->scram_ServerKey,
+                         sizeof(MyProcPort->scram_ServerKey),
+                         (char *) values[n], len);
+           n++;
+       }
+
        keywords[n] = values[n] = NULL;
 
-       /* verify the set of connection parameters */
-       check_conn_params(keywords, values, user);
+       /*
+        * Verify the set of connection parameters only if scram pass-through
+        * is not being used because the password is not necessary.
+        */
+       if (!(MyProcPort->has_scram_keys && UseScramPassthrough(server, user)))
+           check_conn_params(keywords, values, user);
 
        /* first time, allocate or get the custom wait event */
        if (pgfdw_we_connect == 0)
@@ -575,8 +604,12 @@ connect_pg_server(ForeignServer *server, UserMapping *user)
                            server->servername),
                     errdetail_internal("%s", pchomp(PQerrorMessage(conn)))));
 
-       /* Perform post-connection security checks */
-       pgfdw_security_check(keywords, values, user, conn);
+       /*
+        * Perform post-connection security checks only if scram pass-through
+        * is not being used because the password is not necessary.
+        */
+       if (!(MyProcPort->has_scram_keys && UseScramPassthrough(server, user)))
+           pgfdw_security_check(keywords, values, user, conn);
 
        /* Prepare new session for use */
        configure_remote_session(conn);
@@ -629,6 +662,30 @@ UserMappingPasswordRequired(UserMapping *user)
    return true;
 }
 
+static bool
+UseScramPassthrough(ForeignServer *server, UserMapping *user)
+{
+   ListCell   *cell;
+
+   foreach(cell, server->options)
+   {
+       DefElem    *def = (DefElem *) lfirst(cell);
+
+       if (strcmp(def->defname, "use_scram_passthrough") == 0)
+           return defGetBoolean(def);
+   }
+
+   foreach(cell, user->options)
+   {
+       DefElem    *def = (DefElem *) lfirst(cell);
+
+       if (strcmp(def->defname, "use_scram_passthrough") == 0)
+           return defGetBoolean(def);
+   }
+
+   return false;
+}
+
 /*
  * For non-superusers, insist that the connstr specify a password or that the
  * user provided their own GSSAPI delegated credentials.  This
@@ -666,7 +723,7 @@ check_conn_params(const char **keywords, const char **values, UserMapping *user)
    ereport(ERROR,
            (errcode(ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED),
             errmsg("password or GSSAPI delegated credentials required"),
-            errdetail("Non-superusers must delegate GSSAPI credentials or provide a password in the user mapping.")));
+            errdetail("Non-superusers must delegate GSSAPI credentials, provide a password, or enable SCRAM pass-through in user mapping.")));
 }
 
 /*
index bf322198a201aad24af2172b3f50056c6d78a6dc..64aa12ecc484c9cf729765d44701b50778130297 100644 (file)
@@ -10301,7 +10301,7 @@ CREATE FOREIGN TABLE pg_temp.ft1_nopw (
 ) SERVER loopback_nopw OPTIONS (schema_name 'public', table_name 'ft1');
 SELECT 1 FROM ft1_nopw LIMIT 1;
 ERROR:  password or GSSAPI delegated credentials required
-DETAIL:  Non-superusers must delegate GSSAPI credentials or provide a password in the user mapping.
+DETAIL:  Non-superusers must delegate GSSAPI credentials, provide a password, or enable SCRAM pass-through in user mapping.
 -- If we add a password to the connstr it'll fail, because we don't allow passwords
 -- in connstrs only in user mappings.
 ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw');
@@ -10351,7 +10351,7 @@ DROP USER MAPPING FOR CURRENT_USER SERVER loopback_nopw;
 -- lacks password_required=false
 SELECT 1 FROM ft1_nopw LIMIT 1;
 ERROR:  password or GSSAPI delegated credentials required
-DETAIL:  Non-superusers must delegate GSSAPI credentials or provide a password in the user mapping.
+DETAIL:  Non-superusers must delegate GSSAPI credentials, provide a password, or enable SCRAM pass-through in user mapping.
 RESET ROLE;
 -- The user mapping for public is passwordless and lacks the password_required=false
 -- mapping option, but will work because the current user is a superuser.
index 3f19981cffcbffd7f16f234cbf4587d451b37b95..8b29be24deeb74219699aa62245c6fd3aec8aae9 100644 (file)
@@ -41,4 +41,9 @@ tests += {
     ],
     'regress_args': ['--dlpath', meson.build_root() / 'src/test/regress'],
   },
+  'tap': {
+    'tests': [
+      't/001_auth_scram.pl',
+    ],
+  },
 }
index 12aed4054fa26d495390e9792db1d29dccb5925e..d0766f007d2f9ab2f565ae59202b3b98321fc2ed 100644 (file)
@@ -279,6 +279,9 @@ InitPgFdwOptions(void)
        {"analyze_sampling", ForeignServerRelationId, false},
        {"analyze_sampling", ForeignTableRelationId, false},
 
+       {"use_scram_passthrough", ForeignServerRelationId, false},
+       {"use_scram_passthrough", UserMappingRelationId, false},
+
        /*
         * sslcert and sslkey are in fact libpq options, but we repeat them
         * here to allow them to appear in both foreign server context (when
diff --git a/contrib/postgres_fdw/t/001_auth_scram.pl b/contrib/postgres_fdw/t/001_auth_scram.pl
new file mode 100644 (file)
index 0000000..047840c
--- /dev/null
@@ -0,0 +1,151 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+# Test SCRAM authentication when opening a new connection with a foreign
+# server.
+#
+# The test is executed by testing the SCRAM authentifcation on a looplback
+# connection on the same server and with different servers.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+my $hostaddr = '127.0.0.1';
+my $user = "user01";
+
+my $db0 = "db0";    # For node1
+my $db1 = "db1";    # For node1
+my $db2 = "db2";    # For node2
+my $fdw_server = "db1_fdw";
+my $fdw_server2 = "db2_fdw";
+
+my $node1 = PostgreSQL::Test::Cluster->new('node1');
+my $node2 = PostgreSQL::Test::Cluster->new('node2');
+
+$node1->init;
+$node2->init;
+
+$node1->start;
+$node2->start;
+
+# Test setup
+
+$node1->safe_psql('postgres', qq'CREATE USER $user WITH password \'pass\'');
+$node2->safe_psql('postgres', qq'CREATE USER $user WITH password \'pass\'');
+$ENV{PGPASSWORD} = "pass";
+
+$node1->safe_psql('postgres', qq'CREATE DATABASE $db0');
+$node1->safe_psql('postgres', qq'CREATE DATABASE $db1');
+$node2->safe_psql('postgres', qq'CREATE DATABASE $db2');
+
+setup_table($node1, $db1, "t");
+setup_table($node2, $db2, "t2");
+
+$node1->safe_psql($db0, 'CREATE EXTENSION IF NOT EXISTS postgres_fdw');
+setup_fdw_server($node1, $db0, $fdw_server, $node1, $db1);
+setup_fdw_server($node1, $db0, $fdw_server2, $node2, $db2);
+
+setup_user_mapping($node1, $db0, $fdw_server);
+setup_user_mapping($node1, $db0, $fdw_server2);
+
+# Make the user have the same SCRAM key on both servers. Forcing to have the
+# same iteration and salt.
+my $rolpassword = $node1->safe_psql('postgres',
+   qq"SELECT rolpassword FROM pg_authid WHERE rolname = '$user';");
+$node2->safe_psql('postgres', qq"ALTER ROLE $user PASSWORD '$rolpassword'");
+
+setup_pghba($node1);
+setup_pghba($node2);
+
+# End of test setup
+
+test_fdw_auth($node1, $db0, "t", $fdw_server,
+   "SCRAM auth on the same database cluster must succeed");
+test_fdw_auth($node1, $db0, "t2", $fdw_server2,
+   "SCRAM auth on a different database cluster must succeed");
+test_auth($node2, $db2, "t2",
+   "SCRAM auth directly on foreign server should still succeed");
+
+# Helper functions
+
+sub test_auth
+{
+   local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+   my ($node, $db, $tbl, $testname) = @_;
+   my $connstr = $node->connstr($db) . qq' user=$user';
+
+   my $ret = $node->safe_psql(
+       $db,
+       qq'SELECT count(1) FROM $tbl',
+       connstr => $connstr);
+
+   is($ret, '10', $testname);
+}
+
+sub test_fdw_auth
+{
+   local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+   my ($node, $db, $tbl, $fdw, $testname) = @_;
+   my $connstr = $node->connstr($db) . qq' user=$user';
+
+   $node->safe_psql(
+       $db,
+       qq'IMPORT FOREIGN SCHEMA public LIMIT TO ($tbl) FROM SERVER $fdw INTO public;',
+       connstr => $connstr);
+
+   test_auth($node, $db, $tbl, $testname);
+}
+
+sub setup_pghba
+{
+   my ($node) = @_;
+
+   unlink($node->data_dir . '/pg_hba.conf');
+   $node->append_conf(
+       'pg_hba.conf', qq{
+   local   all             all                                     scram-sha-256
+   host    all             all             $hostaddr/32            scram-sha-256
+   });
+
+   $node->restart;
+}
+
+sub setup_user_mapping
+{
+   my ($node, $db, $fdw) = @_;
+
+   $node->safe_psql($db,
+       qq'CREATE USER MAPPING FOR $user SERVER $fdw OPTIONS (user \'$user\');'
+   );
+   $node->safe_psql($db, qq'GRANT USAGE ON FOREIGN SERVER $fdw TO $user;');
+   $node->safe_psql($db, qq'GRANT ALL ON SCHEMA public TO $user');
+}
+
+sub setup_fdw_server
+{
+   my ($node, $db, $fdw, $fdw_node, $dbname) = @_;
+   my $host = $fdw_node->host;
+   my $port = $fdw_node->port;
+
+   $node->safe_psql(
+       $db, qq'CREATE SERVER $fdw FOREIGN DATA WRAPPER postgres_fdw options (
+       host \'$host\', port \'$port\', dbname \'$dbname\', use_scram_passthrough \'true\') '
+   );
+}
+
+sub setup_table
+{
+   my ($node, $db, $tbl) = @_;
+
+   $node->safe_psql($db,
+       qq'CREATE TABLE $tbl AS SELECT g, g + 1 FROM generate_series(1,10) g(g)'
+   );
+   $node->safe_psql($db, qq'GRANT USAGE ON SCHEMA public TO $user');
+   $node->safe_psql($db, qq'GRANT SELECT ON $tbl TO $user');
+}
+
+done_testing();
index 105b22b31715176cf804b5b60be8a706619a18d9..e04acf1c2082938f30bb927d4d0e61ebd431ed6a 100644 (file)
@@ -2199,6 +2199,34 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-scram-client-key" xreflabel="scram_client_key">
+      <term><literal>scram_client_key</literal></term>
+      <listitem>
+       <para>
+        The base64-encoded SCRAM client key.  This can be used by foreign-data
+        wrappers or similar middleware to enable pass-through SCRAM
+        authentication. See <xref
+        linkend="postgres-fdw-options-connection-management"/> for one such
+        implementation.  It is not meant to be specified directly by users or
+        client applications.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-scram-server-key" xreflabel="scram_server_key">
+      <term><literal>scram_server_key</literal></term>
+      <listitem>
+       <para>
+        The base64-encoded SCRAM server key.  This can be used by foreign-data
+        wrappers or similar middleware to enable pass-through SCRAM
+        authentication. See <xref
+        linkend="postgres-fdw-options-connection-management"/> for one such
+        implementation.  It is not meant to be specified directly by users or
+        client applications.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-service" xreflabel="service">
       <term><literal>service</literal></term>
       <listitem>
index 188e8f0b4d0e862edb0d218b8e341a90db98da88..d2998c13d5dd29094b74a9c59379275551177652 100644 (file)
@@ -770,6 +770,78 @@ OPTIONS (ADD password_required 'false');
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><literal>use_scram_passthrough</literal> (<type>boolean</type>)</term>
+      <listitem>
+       <para>
+        This option controls whether <filename>postgres_fdw</filename> will
+        use the SCRAM pass-through authentication to connect to the foreign
+        server.  With SCRAM pass-through authentication,
+        <filename>postgres_fdw</filename> uses SCRAM-hashed secrets instead of
+        plain-text user passwords to connect to the remote server.  This
+        avoids storing plain-text user passwords in PostgreSQL system
+        catalogs.
+       </para>
+
+       <para>
+        To use SCRAM pass-through authentication:
+        <itemizedlist>
+         <listitem>
+          <para>
+           The remote server must request SCRAM authentication.  (If desired,
+           enforce this on the client side (FDW side) with the option
+           <literal>require_auth</literal>.)  If another authentication method
+           is requested by the server, then that one will be used normally.
+          </para>
+         </listitem>
+
+         <listitem>
+          <para>
+           The remote server can be of any PostgreSQL version that supports
+           SCRAM.  Support for <literal>use_scram_passthrough</literal> is
+           only required on the client side (FDW side).
+          </para>
+         </listitem>
+
+         <listitem>
+          <para>
+           The user mapping password is not used.  (It could be set to support
+           other authentication methods, but that would arguably violate the
+           point of this feature, which is to avoid storing plain-text
+           passwords.)
+          </para>
+         </listitem>
+
+         <listitem>
+          <para>
+           The server running <filename>postgres_fdw</filename> and the remote
+           server must have identical SCRAM secrets (encrypted passwords) for
+           the user being used on <filename>postgres_fdw</filename> to
+           authenticate on the foreign server (same salt and iterations, not
+           merely the same password).
+          </para>
+
+          <para>
+           As a corollary, if FDW connections to multiple hosts are to be
+           made, for example for partitioned foreign tables/sharding, then all
+           hosts must have identical SCRAM secrets for the users involved.
+          </para>
+         </listitem>
+
+         <listitem>
+          <para>
+           The current session on the PostgreSQL instance that makes the
+           outgoing FDW connections also must also use SCRAM authentication
+           for its incoming client connection.  (Hence
+           <quote>pass-through</quote>: SCRAM must be used going in and out.)
+           This is a technical requirement of the SCRAM protocol.
+          </para>
+         </listitem>
+        </itemizedlist>
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
    </sect3>
  </sect2>
index 1514133acdc9c5194fbbe5226d39bd8092d74d03..26dd241efa9b31770a1beaa3e8ccee4d71a4c426 100644 (file)
 #include "libpq/crypt.h"
 #include "libpq/sasl.h"
 #include "libpq/scram.h"
+#include "miscadmin.h"
 
 static void scram_get_mechanisms(Port *port, StringInfo buf);
 static void *scram_init(Port *port, const char *selected_mech,
@@ -144,6 +145,7 @@ typedef struct
 
    int         iterations;
    char       *salt;           /* base64-encoded */
+   uint8       ClientKey[SCRAM_MAX_KEY_LEN];
    uint8       StoredKey[SCRAM_MAX_KEY_LEN];
    uint8       ServerKey[SCRAM_MAX_KEY_LEN];
 
@@ -462,6 +464,13 @@ scram_exchange(void *opaq, const char *input, int inputlen,
    if (*output)
        *outputlen = strlen(*output);
 
+   if (result == PG_SASL_EXCHANGE_SUCCESS && state->state == SCRAM_AUTH_FINISHED)
+   {
+       memcpy(MyProcPort->scram_ClientKey, state->ClientKey, sizeof(MyProcPort->scram_ClientKey));
+       memcpy(MyProcPort->scram_ServerKey, state->ServerKey, sizeof(MyProcPort->scram_ServerKey));
+       MyProcPort->has_scram_keys = true;
+   }
+
    return result;
 }
 
@@ -1140,7 +1149,6 @@ static bool
 verify_client_proof(scram_state *state)
 {
    uint8       ClientSignature[SCRAM_MAX_KEY_LEN];
-   uint8       ClientKey[SCRAM_MAX_KEY_LEN];
    uint8       client_StoredKey[SCRAM_MAX_KEY_LEN];
    pg_hmac_ctx *ctx = pg_hmac_create(state->hash_type);
    int         i;
@@ -1173,10 +1181,10 @@ verify_client_proof(scram_state *state)
 
    /* Extract the ClientKey that the client calculated from the proof */
    for (i = 0; i < state->key_length; i++)
-       ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i];
+       state->ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i];
 
    /* Hash it one more time, and compare with StoredKey */
-   if (scram_H(ClientKey, state->hash_type, state->key_length,
+   if (scram_H(state->ClientKey, state->hash_type, state->key_length,
                client_StoredKey, &errstr) < 0)
        elog(ERROR, "could not hash stored key: %s", errstr);
 
index 16da6f89ef115a47672bc1a571d5e8778ae06432..2f6c29200ba65fd217d4637c8f895f4b55fefd72 100644 (file)
@@ -18,6 +18,8 @@
 #ifndef LIBPQ_BE_H
 #define LIBPQ_BE_H
 
+#include "common/scram-common.h"
+
 #include <sys/time.h>
 #ifdef USE_OPENSSL
 #include <openssl/ssl.h>
@@ -181,6 +183,13 @@ typedef struct Port
    int         keepalives_count;
    int         tcp_user_timeout;
 
+   /*
+    * SCRAM structures.
+    */
+   uint8       scram_ClientKey[SCRAM_MAX_KEY_LEN];
+   uint8       scram_ServerKey[SCRAM_MAX_KEY_LEN];
+   bool        has_scram_keys; /* true if the above two are valid */
+
    /*
     * GSSAPI structures.
     */
index 59bf87d2213e9c4b0036fe6e01427cfd531c2a46..557e9c568b66990897af39ddf2833d4c144360aa 100644 (file)
@@ -119,25 +119,28 @@ scram_init(PGconn *conn,
        return NULL;
    }
 
-   /* Normalize the password with SASLprep, if possible */
-   rc = pg_saslprep(password, &prep_password);
-   if (rc == SASLPREP_OOM)
-   {
-       free(state->sasl_mechanism);
-       free(state);
-       return NULL;
-   }
-   if (rc != SASLPREP_SUCCESS)
+   if (password)
    {
-       prep_password = strdup(password);
-       if (!prep_password)
+       /* Normalize the password with SASLprep, if possible */
+       rc = pg_saslprep(password, &prep_password);
+       if (rc == SASLPREP_OOM)
        {
            free(state->sasl_mechanism);
            free(state);
            return NULL;
        }
+       if (rc != SASLPREP_SUCCESS)
+       {
+           prep_password = strdup(password);
+           if (!prep_password)
+           {
+               free(state->sasl_mechanism);
+               free(state);
+               return NULL;
+           }
+       }
+       state->password = prep_password;
    }
-   state->password = prep_password;
 
    return state;
 }
@@ -775,20 +778,31 @@ calculate_client_proof(fe_scram_state *state,
        return false;
    }
 
-   /*
-    * Calculate SaltedPassword, and store it in 'state' so that we can reuse
-    * it later in verify_server_signature.
-    */
-   if (scram_SaltedPassword(state->password, state->hash_type,
-                            state->key_length, state->salt, state->saltlen,
-                            state->iterations, state->SaltedPassword,
-                            errstr) < 0 ||
-       scram_ClientKey(state->SaltedPassword, state->hash_type,
-                       state->key_length, ClientKey, errstr) < 0 ||
-       scram_H(ClientKey, state->hash_type, state->key_length,
-               StoredKey, errstr) < 0)
-   {
-       /* errstr is already filled here */
+   if (state->conn->scram_client_key_binary)
+   {
+       memcpy(ClientKey, state->conn->scram_client_key_binary, SCRAM_MAX_KEY_LEN);
+   }
+   else
+   {
+       /*
+        * Calculate SaltedPassword, and store it in 'state' so that we can
+        * reuse it later in verify_server_signature.
+        */
+       if (scram_SaltedPassword(state->password, state->hash_type,
+                                state->key_length, state->salt, state->saltlen,
+                                state->iterations, state->SaltedPassword,
+                                errstr) < 0 ||
+           scram_ClientKey(state->SaltedPassword, state->hash_type,
+                           state->key_length, ClientKey, errstr) < 0)
+       {
+           /* errstr is already filled here */
+           pg_hmac_free(ctx);
+           return false;
+       }
+   }
+
+   if (scram_H(ClientKey, state->hash_type, state->key_length, StoredKey, errstr) < 0)
+   {
        pg_hmac_free(ctx);
        return false;
    }
@@ -841,12 +855,19 @@ verify_server_signature(fe_scram_state *state, bool *match,
        return false;
    }
 
-   if (scram_ServerKey(state->SaltedPassword, state->hash_type,
-                       state->key_length, ServerKey, errstr) < 0)
+   if (state->conn->scram_server_key_binary)
    {
-       /* errstr is filled already */
-       pg_hmac_free(ctx);
-       return false;
+       memcpy(ServerKey, state->conn->scram_server_key_binary, SCRAM_MAX_KEY_LEN);
+   }
+   else
+   {
+       if (scram_ServerKey(state->SaltedPassword, state->hash_type,
+                           state->key_length, ServerKey, errstr) < 0)
+       {
+           /* errstr is filled already */
+           pg_hmac_free(ctx);
+           return false;
+       }
    }
 
    /* calculate ServerSignature */
index 14a9a862f51d040b8a501e250ec8122de784fbd4..7e478489b71a1451b79841d58d063a3c2e0a080d 100644 (file)
@@ -559,7 +559,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
     * First, select the password to use for the exchange, complaining if
     * there isn't one and the selected SASL mechanism needs it.
     */
-   if (conn->password_needed)
+   if (conn->password_needed && !conn->scram_client_key_binary)
    {
        password = conn->connhost[conn->whichhost].password;
        if (password == NULL)
index 8f211821eb295647cfaa9d0ebcfdc56daa3e05fe..c7943d549e88462a2d60d05d8c6dcf3061103895 100644 (file)
@@ -22,6 +22,7 @@
 #include <time.h>
 #include <unistd.h>
 
+#include "common/base64.h"
 #include "common/ip.h"
 #include "common/link-canary.h"
 #include "common/scram-common.h"
@@ -366,6 +367,12 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
        "Load-Balance-Hosts", "", 8,    /* sizeof("disable") = 8 */
    offsetof(struct pg_conn, load_balance_hosts)},
 
+   {"scram_client_key", NULL, NULL, NULL, "SCRAM-Client-Key", "D", SCRAM_MAX_KEY_LEN * 2,
+   offsetof(struct pg_conn, scram_client_key)},
+
+   {"scram_server_key", NULL, NULL, NULL, "SCRAM-Server-Key", "D", SCRAM_MAX_KEY_LEN * 2,
+   offsetof(struct pg_conn, scram_server_key)},
+
    /* Terminating entry --- MUST BE LAST */
    {NULL, NULL, NULL, NULL,
    NULL, NULL, 0}
@@ -1793,6 +1800,44 @@ pqConnectOptions2(PGconn *conn)
    else
        conn->target_server_type = SERVER_TYPE_ANY;
 
+   if (conn->scram_client_key)
+   {
+       int         len;
+
+       len = pg_b64_dec_len(strlen(conn->scram_client_key));
+       /* Consider the zero-terminator */
+       if (len != SCRAM_MAX_KEY_LEN + 1)
+       {
+           libpq_append_conn_error(conn, "invalid SCRAM client key length: %d", len);
+           return false;
+       }
+       conn->scram_client_key_len = len;
+       conn->scram_client_key_binary = malloc(len);
+       if (!conn->scram_client_key_binary)
+           goto oom_error;
+       pg_b64_decode(conn->scram_client_key, strlen(conn->scram_client_key),
+                     conn->scram_client_key_binary, len);
+   }
+
+   if (conn->scram_server_key)
+   {
+       int         len;
+
+       len = pg_b64_dec_len(strlen(conn->scram_server_key));
+       /* Consider the zero-terminator */
+       if (len != SCRAM_MAX_KEY_LEN + 1)
+       {
+           libpq_append_conn_error(conn, "invalid SCRAM server key length: %d", len);
+           return false;
+       }
+       conn->scram_server_key_len = len;
+       conn->scram_server_key_binary = malloc(len);
+       if (!conn->scram_server_key_binary)
+           goto oom_error;
+       pg_b64_decode(conn->scram_server_key, strlen(conn->scram_server_key),
+                     conn->scram_server_key_binary, len);
+   }
+
    /*
     * validate load_balance_hosts option, and set load_balance_type
     */
@@ -4704,6 +4749,8 @@ freePGconn(PGconn *conn)
    free(conn->rowBuf);
    free(conn->target_session_attrs);
    free(conn->load_balance_hosts);
+   free(conn->scram_client_key);
+   free(conn->scram_server_key);
    termPQExpBuffer(&conn->errorMessage);
    termPQExpBuffer(&conn->workBuffer);
 
index 4a5a7c8b5e35d86f02d2b5bee291eac225cd0c6f..031b93c535b9422a2b91337f0a5e44e8664ec053 100644 (file)
@@ -428,6 +428,8 @@ struct pg_conn
    char       *target_session_attrs;   /* desired session properties */
    char       *require_auth;   /* name of the expected auth method */
    char       *load_balance_hosts; /* load balance over hosts */
+   char       *scram_client_key;   /* base64-encoded SCRAM client key */
+   char       *scram_server_key;   /* base64-encoded SCRAM server key */
 
    bool        cancelRequest;  /* true if this connection is used to send a
                                 * cancel request, instead of being a normal
@@ -518,6 +520,10 @@ struct pg_conn
    AddrInfo   *addr;           /* the array of addresses for the currently
                                 * tried host */
    bool        send_appname;   /* okay to send application_name? */
+   size_t      scram_client_key_len;
+   void       *scram_client_key_binary;    /* binary SCRAM client key */
+   size_t      scram_server_key_len;
+   void       *scram_server_key_binary;    /* binary SCRAM server key */
 
    /* Miscellaneous stuff */
    int         be_pid;         /* PID of backend --- needed for cancels */