Make SCRAM iteration count configurable
authorDaniel Gustafsson <dgustafsson@postgresql.org>
Mon, 27 Mar 2023 07:46:29 +0000 (09:46 +0200)
committerDaniel Gustafsson <dgustafsson@postgresql.org>
Mon, 27 Mar 2023 07:46:29 +0000 (09:46 +0200)
Replace the hardcoded value with a GUC such that the iteration
count can be raised in order to increase protection against
brute-force attacks.  The hardcoded value for SCRAM iteration
count was defined to be 4096, which is taken from RFC 7677, so
set the default for the GUC to 4096 to match.  In RFC 7677 the
recommendation is at least 15000 iterations but 4096 is listed
as a SHOULD requirement given that it's estimated to yield a
0.5s processing time on a mobile handset of the time of RFC
writing (late 2015).

Raising the iteration count of SCRAM will make stored passwords
more resilient to brute-force attacks at a higher computational
cost during connection establishment.  Lowering the count will
reduce computational overhead during connections at the tradeoff
of reducing strength against brute-force attacks.

There are however platforms where even a modest iteration count
yields a too high computational overhead, with weaker password
encryption schemes chosen as a result.  In these situations,
SCRAM with a very low iteration count still gives benefits over
weaker schemes like md5, so we allow the iteration count to be
set to one at the low end.

The new GUC is intentionally generically named such that it can
be made to support future SCRAM standards should they emerge.
At that point the value can be made into key:value pairs with
an undefined key as a default which will be backwards compatible
with this.

Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Jonathan S. Katz <jkatz@postgresql.org>
Discussion: https://postgr.es/m/F72E7BC7-189F-4B17-BF47-9735EB72C364@yesql.se

16 files changed:
doc/src/sgml/config.sgml
src/backend/libpq/auth-scram.c
src/backend/utils/misc/guc_tables.c
src/backend/utils/misc/postgresql.conf.sample
src/common/scram-common.c
src/include/common/scram-common.h
src/include/libpq/scram.h
src/interfaces/libpq/fe-auth-scram.c
src/interfaces/libpq/fe-auth.c
src/interfaces/libpq/fe-auth.h
src/interfaces/libpq/fe-connect.c
src/interfaces/libpq/fe-exec.c
src/interfaces/libpq/libpq-int.h
src/test/authentication/t/001_password.pl
src/test/regress/expected/password.out
src/test/regress/sql/password.sql

index 481f93cea1bc0465d4c10979d9cfa35d4bd8f0bd..71730cc52fe7810c26823f98da58078677f2cdf8 100644 (file)
@@ -1132,6 +1132,26 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><varname>scram_iterations</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>scram_iterations</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The number of computational iterations to be performed when encrypting
+        a password using SCRAM-SHA-256. The default is <literal>4096</literal>.
+        A higher number of iterations provides additional protection against
+        brute-force attacks on stored passwords, but makes authentication
+        slower. Changing the value has no effect on existing passwords
+        encrypted with SCRAM-SHA-256 as the iteration count is fixed at the
+        time of encryption. In order to make use of a changed value, a new
+        password must be set.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-krb-server-keyfile" xreflabel="krb_server_keyfile">
       <term><varname>krb_server_keyfile</varname> (<type>string</type>)
       <indexterm>
index 4441e0d7745bf549d6acb26d86b325b983dbae30..9b286aa4d7fad615f7075561bb20474347b45169 100644 (file)
@@ -191,6 +191,11 @@ static char *scram_mock_salt(const char *username,
                             pg_cryptohash_type hash_type,
                             int key_length);
 
+/*
+ * The number of iterations to use when generating new secrets.
+ */
+int            scram_sha_256_iterations = SCRAM_SHA_256_DEFAULT_ITERATIONS;
+
 /*
  * Get a list of SASL mechanisms that this module supports.
  *
@@ -496,7 +501,7 @@ pg_be_scram_build_secret(const char *password)
 
    result = scram_build_secret(PG_SHA256, SCRAM_SHA_256_KEY_LEN,
                                saltbuf, SCRAM_DEFAULT_SALT_LEN,
-                               SCRAM_DEFAULT_ITERATIONS, password,
+                               scram_sha_256_iterations, password,
                                &errstr);
 
    if (prep_password)
@@ -717,7 +722,7 @@ mock_scram_secret(const char *username, pg_cryptohash_type *hash_type,
    encoded_salt[encoded_len] = '\0';
 
    *salt = encoded_salt;
-   *iterations = SCRAM_DEFAULT_ITERATIONS;
+   *iterations = SCRAM_SHA_256_DEFAULT_ITERATIONS;
 
    /* StoredKey and ServerKey are not used in a doomed authentication */
    memset(stored_key, 0, SCRAM_MAX_KEY_LEN);
index 1c0583fe2676e5568740350e1c0a9606774a3f3c..a60bd48499e3708a70a0053d1b4f1dffc4d36b56 100644 (file)
 #include "commands/trigger.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
+#include "common/scram-common.h"
 #include "jit/jit.h"
 #include "libpq/auth.h"
 #include "libpq/libpq.h"
+#include "libpq/scram.h"
 #include "nodes/queryjumble.h"
 #include "optimizer/cost.h"
 #include "optimizer/geqo.h"
@@ -3468,6 +3470,17 @@ struct config_int ConfigureNamesInt[] =
        NULL, NULL, NULL
    },
 
+   {
+       {"scram_iterations", PGC_USERSET, CONN_AUTH_AUTH,
+           gettext_noop("Sets the iteration count for SCRAM secret generation."),
+           NULL,
+           GUC_REPORT
+       },
+       &scram_sha_256_iterations,
+       SCRAM_SHA_256_DEFAULT_ITERATIONS, 1, INT_MAX,
+       NULL, NULL, NULL
+   },
+
    /* End-of-list marker */
    {
        {NULL, 0, 0, NULL, NULL}, NULL, 0, 0, 0, NULL, NULL, NULL
index d06074b86f6de8fce9dedffc1207685dcf5900e8..fc831565d90bf471e1c7bc4a022c3853c8cb5ab5 100644 (file)
@@ -95,6 +95,7 @@
 
 #authentication_timeout = 1min     # 1s-600s
 #password_encryption = scram-sha-256   # scram-sha-256 or md5
+#scram_iterations = 4096
 #db_user_namespace = off
 
 # GSSAPI using Kerberos
index bd40d497a9606ecfc91603ae9a11db73b8eb9d8c..ef997ef6849206f20c9eeb3ec028a17e731e0335 100644 (file)
@@ -214,8 +214,7 @@ scram_build_secret(pg_cryptohash_type hash_type, int key_length,
    /* Only this hash method is supported currently */
    Assert(hash_type == PG_SHA256);
 
-   if (iterations <= 0)
-       iterations = SCRAM_DEFAULT_ITERATIONS;
+   Assert(iterations > 0);
 
    /* Calculate StoredKey and ServerKey */
    if (scram_SaltedPassword(password, hash_type, key_length,
index 0c75df555974c46277678d0e57fe8384d2650bd7..5ccff96eceec0dfefa02fd7b1b182b266a0d443d 100644 (file)
@@ -47,7 +47,7 @@
  * Default number of iterations when generating secret.  Should be at least
  * 4096 per RFC 7677.
  */
-#define SCRAM_DEFAULT_ITERATIONS   4096
+#define SCRAM_SHA_256_DEFAULT_ITERATIONS   4096
 
 extern int scram_SaltedPassword(const char *password,
                                 pg_cryptohash_type hash_type, int key_length,
index b275e1e87e554a97b71542cfb0ac045ca5638e6c..310bc36517707c5b4abb28dbc346c2ea06ae3192 100644 (file)
@@ -18,6 +18,9 @@
 #include "libpq/libpq-be.h"
 #include "libpq/sasl.h"
 
+/* Number of iterations when generating new secrets */
+extern PGDLLIMPORT int scram_sha_256_iterations;
+
 /* SASL implementation callbacks */
 extern PGDLLIMPORT const pg_be_sasl_mech pg_be_scram_mech;
 
index 277f72b280c62f03ee66ce71873a91d8a1da6995..6b779ec7ffd1e77bae0d9bf9d16c658d302132e5 100644 (file)
@@ -895,7 +895,7 @@ verify_server_signature(fe_scram_state *state, bool *match,
  * error details.
  */
 char *
-pg_fe_scram_build_secret(const char *password, const char **errstr)
+pg_fe_scram_build_secret(const char *password, int iterations, const char **errstr)
 {
    char       *prep_password;
    pg_saslprep_rc rc;
@@ -927,7 +927,7 @@ pg_fe_scram_build_secret(const char *password, const char **errstr)
 
    result = scram_build_secret(PG_SHA256, SCRAM_SHA_256_KEY_LEN, saltbuf,
                                SCRAM_DEFAULT_SALT_LEN,
-                               SCRAM_DEFAULT_ITERATIONS, password,
+                               iterations, password,
                                errstr);
 
    free(prep_password);
index 934e3f4f7ca89b95921d04ef1544d96e71c62152..b0550e633241d8d8004db6cb34102ae1bf83152a 100644 (file)
@@ -1341,7 +1341,9 @@ PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user,
    {
        const char *errstr = NULL;
 
-       crypt_pwd = pg_fe_scram_build_secret(passwd, &errstr);
+       crypt_pwd = pg_fe_scram_build_secret(passwd,
+                                            conn->scram_sha_256_iterations,
+                                            &errstr);
        if (!crypt_pwd)
            libpq_append_conn_error(conn, "could not encrypt password: %s", errstr);
    }
index 1aa556ea2fa80560d293222a8fac70ea7e336c4d..124dd5d031332888e6901374c4b7acb8dacefe8e 100644 (file)
@@ -26,6 +26,7 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
 /* Mechanisms in fe-auth-scram.c */
 extern const pg_fe_sasl_mech pg_scram_mech;
 extern char *pg_fe_scram_build_secret(const char *password,
+                                     int iterations,
                                      const char **errstr);
 
 #endif                         /* FE_AUTH_H */
index 660775e0198310706406e9e58dae46ec4691664e..b71378d94c5d5673c18c5aee194c489deb8c87d2 100644 (file)
@@ -596,6 +596,7 @@ pqDropServerData(PGconn *conn)
    conn->std_strings = false;
    conn->default_transaction_read_only = PG_BOOL_UNKNOWN;
    conn->in_hot_standby = PG_BOOL_UNKNOWN;
+   conn->scram_sha_256_iterations = SCRAM_SHA_256_DEFAULT_ITERATIONS;
    conn->sversion = 0;
 
    /* Drop large-object lookup data */
@@ -4182,6 +4183,7 @@ makeEmptyPGconn(void)
    conn->std_strings = false;  /* unless server says differently */
    conn->default_transaction_read_only = PG_BOOL_UNKNOWN;
    conn->in_hot_standby = PG_BOOL_UNKNOWN;
+   conn->scram_sha_256_iterations = SCRAM_SHA_256_DEFAULT_ITERATIONS;
    conn->verbosity = PQERRORS_DEFAULT;
    conn->show_context = PQSHOW_CONTEXT_ERRORS;
    conn->sock = PGINVALID_SOCKET;
index ec62550e3858135a48be0489df51883c0f9b25ca..a16bbf32ef5c0043eee9c92ab82bf4f11386ee47 100644 (file)
@@ -1181,6 +1181,10 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
        conn->in_hot_standby =
            (strcmp(value, "on") == 0) ? PG_BOOL_YES : PG_BOOL_NO;
    }
+   else if (strcmp(name, "scram_iterations") == 0)
+   {
+       conn->scram_sha_256_iterations = atoi(value);
+   }
 }
 
 
index 10187c31b9a90d6f35e18a1e0abf0b90975415b1..88b9838d766823714d4bb5722829181c55e2edd8 100644 (file)
@@ -525,6 +525,7 @@ struct pg_conn
    /* Assorted state for SASL, SSL, GSS, etc */
    const pg_fe_sasl_mech *sasl;
    void       *sasl_state;
+   int         scram_sha_256_iterations;
 
    /* SSL structures */
    bool        ssl_in_use;
index cba5d7d6487070ba9572bbb9247c2e7157f9efd7..00857fdae5a4128fdcae5ab3e32d264c5ddf6498 100644 (file)
@@ -86,6 +86,21 @@ $node->safe_psql('postgres',
    q{SET password_encryption='md5'; CREATE ROLE "md5,role" LOGIN PASSWORD 'pass';}
 );
 
+# Create a role with a non-default iteration count
+$node->safe_psql(
+   'postgres',
+   "SET password_encryption='scram-sha-256';
+    SET scram_iterations=1024;
+    CREATE ROLE scram_role_iter LOGIN PASSWORD 'pass';
+    RESET scram_iterations;"
+);
+
+my $res = $node->safe_psql('postgres',
+   "SELECT substr(rolpassword,1,19)
+    FROM pg_authid
+    WHERE rolname = 'scram_role_iter'");
+is($res, 'SCRAM-SHA-256$1024:', 'scram_iterations in server side ROLE');
+
 # Create a database to test regular expression.
 $node->safe_psql('postgres', "CREATE database regex_testdb;");
 
@@ -98,7 +113,7 @@ test_conn($node, 'user=md5_role', 'trust', 0,
    log_unlike => [qr/connection authenticated:/]);
 
 # SYSTEM_USER is null when not authenticated.
-my $res = $node->safe_psql('postgres', "SELECT SYSTEM_USER IS NULL;");
+$res = $node->safe_psql('postgres', "SELECT SYSTEM_USER IS NULL;");
 is($res, 't', "users with trust authentication use SYSTEM_USER = NULL");
 
 # Test SYSTEM_USER with parallel workers when not authenticated.
@@ -283,6 +298,14 @@ test_conn(
    log_like => [
        qr/connection authenticated: identity="scram_role" method=scram-sha-256/
    ]);
+test_conn(
+   $node,
+   'user=scram_role_iter',
+   'scram-sha-256',
+   0,
+   log_like => [
+       qr/connection authenticated: identity="scram_role_iter" method=scram-sha-256/
+   ]);
 test_conn($node, 'user=md5_role', 'scram-sha-256', 2,
    log_unlike => [qr/connection authenticated:/]);
 
index 7c84c9da33777c50952c78b9605768212c99c19f..847523173567531cf94bc22b5e84774b09337953 100644 (file)
@@ -72,6 +72,9 @@ CREATE ROLE regress_passwd6 PASSWORD 'SCRAM-SHA-256$1234';
 CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
+-- Changing the SCRAM iteration count
+SET scram_iterations = 1024;
+CREATE ROLE regress_passwd9 PASSWORD 'alterediterationcount';
 SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
     WHERE rolname LIKE 'regress_passwd%'
@@ -86,7 +89,8 @@ SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+
  regress_passwd6 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
  regress_passwd7 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
  regress_passwd8 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
-(8 rows)
+ regress_passwd9 | SCRAM-SHA-256$1024:<salt>$<storedkey>:<serverkey>
+(9 rows)
 
 -- An empty password is not allowed, in any form
 CREATE ROLE regress_passwd_empty PASSWORD '';
@@ -129,6 +133,7 @@ DROP ROLE regress_passwd5;
 DROP ROLE regress_passwd6;
 DROP ROLE regress_passwd7;
 DROP ROLE regress_passwd8;
+DROP ROLE regress_passwd9;
 DROP ROLE regress_passwd_empty;
 DROP ROLE regress_passwd_sha_len0;
 DROP ROLE regress_passwd_sha_len1;
index 98f49916e5d5f9d51647a47abfc01d782e6fb09f..53e86b0b6ce9e18912a64d6d229787d5781dccf5 100644 (file)
@@ -63,6 +63,10 @@ CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
 
+-- Changing the SCRAM iteration count
+SET scram_iterations = 1024;
+CREATE ROLE regress_passwd9 PASSWORD 'alterediterationcount';
+
 SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
     WHERE rolname LIKE 'regress_passwd%'
@@ -97,6 +101,7 @@ DROP ROLE regress_passwd5;
 DROP ROLE regress_passwd6;
 DROP ROLE regress_passwd7;
 DROP ROLE regress_passwd8;
+DROP ROLE regress_passwd9;
 DROP ROLE regress_passwd_empty;
 DROP ROLE regress_passwd_sha_len0;
 DROP ROLE regress_passwd_sha_len1;