public inbox for [email protected]  
help / color / mirror / Atom feed
From: Daniel Gustafsson <[email protected]>
To: PostgreSQL Hackers <[email protected]>
Subject: Raising the SCRAM iteration count
Date: Fri, 9 Dec 2022 11:55:07 +0100
Message-ID: <[email protected]> (raw)

In the thread about user space SCRAM functions [0] I mentioned that it might be
wise to consider raising our SCRAM iteration count.  The iteration count is an
important defence against brute-force attacks.

Our current hardcoded value for iteration count is 4096, which is based on a
recommendation from RFC 7677.  This is however the lower end of the scale, and
is related to computing power in 2015 generation handheld devices.  The
relevant paragraph in section 4 of RFC 7677 [1] reads:

   "As a rule of thumb, the hash iteration-count should be such that a modern
   machine will take 0.1 seconds to perform the complete algorithm; however,
   this is unlikely to be practical on mobile devices and other relatively low-
   performance systems.  At the time this was written, the rule of thumb gives
   around 15,000 iterations required; however, a hash iteration- count of 4096
   takes around 0.5 seconds on current mobile handsets."

It goes on to say:

   "..the recommendation of this specification is that the hash iteration- count
   SHOULD be at least 4096, but careful consideration ought to be given to
   using a significantly higher value, particularly where mobile use is less
   important."

Selecting 4096 was thus a conservative take already in 2015, and is now very
much so.  On my 2020-vintage Macbook I need ~200k iterations to consume 0.1
seconds (in a build with assertions).  Calculating tens of thousands of hashes
per second on a consumer laptop at a 4096 iteration count is no stretch.  A
brief look shows that MongoDB has a minimum of 5000 with a default of 15000
[2]; Kafka has a minimum of 4096 [3].

Making the iteration count a configurable setting would allow installations to
raise the iteration count to strengthen against brute force attacks, while
still supporting those with lower end clients who prefer the trade-off of
shorter authentication times.

The attached introduces a scram_iteration_count GUC with a default of 15000
(still conservative, from RFC7677) and a minimum of 4096.  Since the iterations
are stored per secret it can be altered with backwards compatibility.

Clientside the count is still at 4096 to limit the scope of this patch a bit.
For psql it would mean adding options to \password which should be a thread of
its own.  For libpq one can imagine specifying this in the algorithm parameter
passed to PQencryptPasswordConn like "scram-sha-256:100000" or something
similar.  It's premature to pursue those without agreement that we should make
the count configurable though.  If this patch is accepted, I'll work on that
next.

Thoughts?

--
Daniel Gustafsson		https://vmware.com/

[0] [email protected]
[1] https://www.rfc-editor.org/rfc/rfc7677#section-4
[2] https://www.mongodb.com/docs/manual/reference/parameters/#mongodb-parameter-param.scramSHA256Iterati...
[3] https://docs.confluent.io/platform/current/kafka/authentication_sasl/authentication_sasl_scram.html#...



Attachments:

  [application/octet-stream] v1-0001-Make-SCRAM-iteration-count-configurable.patch (11.5K, ../[email protected]/2-v1-0001-Make-SCRAM-iteration-count-configurable.patch)
  download | inline diff:
From 9de52d4408f26a7758f76ddcff0a629522de4ca0 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 8 Dec 2022 14:49:42 +0100
Subject: [PATCH v1] Make SCRAM iteration count configurable

The current hardcoded value for SCRAM iteration count is defined to be
4096,  which is taken from RFC 7677 chapter 4 where it is cited as the
highest number of iterations for mobile units.  This data is however 7
years old by know, and most postgres authentications are made on hand-
held devices.

Replace the hardcoded value with a GUC such that the iteration count
can be raised in order to increase protection against brute-force
attacks.

Discussion: https://postgr.es/m/tbd
---
 doc/src/sgml/config.sgml                      | 17 +++++++++++++
 src/backend/libpq/auth-scram.c                |  9 +++++--
 src/backend/utils/misc/guc_tables.c           | 12 +++++++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/common/scram-common.c                     |  4 +--
 src/include/common/scram-common.h             | 17 ++++++++++---
 src/test/authentication/t/001_password.pl     | 16 ++++++++++++
 src/test/regress/expected/password.out        | 25 +++++++++++--------
 src/test/regress/sql/password.sql             |  5 ++++
 9 files changed, 89 insertions(+), 17 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index ff6fcd902a..2e2e85f6b2 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1098,6 +1098,23 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><varname>scram_iteration_count</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>scram_iteration_count</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The number of computational iterations to be performed when generating
+        a SCRAM secret. The default is <literal>15000</literal>. A higher
+        number of iterations will provide additional protection against
+        brute-force attacks on stored passwords but will make authentication
+        slower.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-krb-server-keyfile" xreflabel="krb_server_keyfile">
       <term><varname>krb_server_keyfile</varname> (<type>string</type>)
       <indexterm>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index ee7f52218a..0df8a96751 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -184,6 +184,11 @@ static char *sanitize_char(char c);
 static char *sanitize_str(const char *s);
 static char *scram_mock_salt(const char *username);
 
+/*
+ * The number of iterations to use when generating new secrets.
+ */
+int			scram_iteration_count;
+
 /*
  * Get a list of SASL mechanisms that this module supports.
  *
@@ -483,7 +488,7 @@ pg_be_scram_build_secret(const char *password)
 				 errmsg("could not generate random salt")));
 
 	result = scram_build_secret(saltbuf, SCRAM_DEFAULT_SALT_LEN,
-								SCRAM_DEFAULT_ITERATIONS, password,
+								scram_iteration_count, password,
 								&errstr);
 
 	if (prep_password)
@@ -692,7 +697,7 @@ mock_scram_secret(const char *username, int *iterations, char **salt,
 	encoded_salt[encoded_len] = '\0';
 
 	*salt = encoded_salt;
-	*iterations = SCRAM_DEFAULT_ITERATIONS;
+	*iterations = scram_iteration_count;
 
 	/* StoredKey and ServerKey are not used in a doomed authentication */
 	memset(stored_key, 0, SCRAM_KEY_LEN);
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1bf14eec66..44e8e39424 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -40,6 +40,7 @@
 #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"
@@ -3423,6 +3424,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"scram_iteration_count", PGC_SUSET, CONN_AUTH_AUTH,
+			gettext_noop("Sets the iteration count for SCRAM secret generation."),
+			NULL,
+			GUC_NOT_IN_SAMPLE | GUC_SUPERUSER_ONLY
+		},
+		&scram_iteration_count,
+		15000, SCRAM_MIN_ITERATIONS, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, 0, 0, 0, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 043864597f..64b7ad6b7a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -94,6 +94,7 @@
 
 #authentication_timeout = 1min		# 1s-600s
 #password_encryption = scram-sha-256	# scram-sha-256 or md5
+#scram_iteration_count = 15000
 #db_user_namespace = off
 
 # GSSAPI using Kerberos
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index 1268625929..c5a0de37f2 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -206,8 +206,8 @@ scram_build_secret(const char *salt, int saltlen, int iterations,
 	int			encoded_server_len;
 	int			encoded_result;
 
-	if (iterations <= 0)
-		iterations = SCRAM_DEFAULT_ITERATIONS;
+	if (iterations <= SCRAM_MIN_ITERATIONS)
+		iterations = SCRAM_MIN_ITERATIONS;
 
 	/* Calculate StoredKey and ServerKey */
 	if (scram_SaltedPassword(password, salt, saltlen, iterations,
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index e1f5e786e0..0922b10052 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -41,10 +41,21 @@
 #define SCRAM_DEFAULT_SALT_LEN		16
 
 /*
- * Default number of iterations when generating secret.  Should be at least
- * 4096 per RFC 7677.
+ * The minimum allowed number of iterations when generating new secrets.
  */
-#define SCRAM_DEFAULT_ITERATIONS	4096
+#define SCRAM_MIN_ITERATIONS		4096
+
+/*
+ * Default number of iterations when generating secret.
+ */
+#define SCRAM_DEFAULT_ITERATIONS	15000
+
+#ifndef FRONTEND
+/*
+ * Number of iterations when generating new secrets.
+ */
+extern PGDLLIMPORT int scram_iteration_count;
+#endif
 
 extern int	scram_SaltedPassword(const char *password, const char *salt,
 								 int saltlen, int iterations, uint8 *result,
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 42d3d4c79b..8870008f05 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -86,6 +86,14 @@ $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_iteration_count=100000;
+	 CREATE ROLE scram_role_iter LOGIN PASSWORD 'pass';"
+);
+
 # Create a database to test regular expression.
 $node->safe_psql('postgres', "CREATE database regex_testdb;");
 
@@ -134,6 +142,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:/]);
 
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index 7c84c9da33..7252c78ab2 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -28,11 +28,11 @@ SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+
     FROM pg_authid
     WHERE rolname LIKE 'regress_passwd%'
     ORDER BY rolname, rolpassword;
-     rolname     |                rolpassword_masked                 
------------------+---------------------------------------------------
+     rolname     |                 rolpassword_masked                 
+-----------------+----------------------------------------------------
  regress_passwd1 | md5783277baca28003b33453252be4dbb34
  regress_passwd2 | md54044304ba511dd062133eb5b4b84a2a3
- regress_passwd3 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
+ regress_passwd3 | SCRAM-SHA-256$15000:<salt>$<storedkey>:<serverkey>
  regress_passwd4 | 
 (4 rows)
 
@@ -72,21 +72,25 @@ CREATE ROLE regress_passwd6 PASSWORD 'SCRAM-SHA-256$1234';
 CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
+-- Increasing the SCRAM iteration count
+SET scram_iteration_count = 99999;
+CREATE ROLE regress_passwd9 PASSWORD 'raisediterationcount';
 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%'
     ORDER BY rolname, rolpassword;
-     rolname     |                rolpassword_masked                 
------------------+---------------------------------------------------
+     rolname     |                 rolpassword_masked                 
+-----------------+----------------------------------------------------
  regress_passwd1 | md5cd3578025fe2c3d7ed1b9a9b26238b70
  regress_passwd2 | md5dfa155cadd5f4ad57860162f3fab9cdb
  regress_passwd3 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
- regress_passwd4 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
+ regress_passwd4 | SCRAM-SHA-256$15000:<salt>$<storedkey>:<serverkey>
  regress_passwd5 | md5e73a4b11df52a6068f8b39f90be36023
- 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_passwd6 | SCRAM-SHA-256$15000:<salt>$<storedkey>:<serverkey>
+ regress_passwd7 | SCRAM-SHA-256$15000:<salt>$<storedkey>:<serverkey>
+ regress_passwd8 | SCRAM-SHA-256$15000:<salt>$<storedkey>:<serverkey>
+ regress_passwd9 | SCRAM-SHA-256$99999:<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;
diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql
index 98f49916e5..6672d58d59 100644
--- a/src/test/regress/sql/password.sql
+++ b/src/test/regress/sql/password.sql
@@ -63,6 +63,10 @@ CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
 
+-- Increasing the SCRAM iteration count
+SET scram_iteration_count = 99999;
+CREATE ROLE regress_passwd9 PASSWORD 'raisediterationcount';
+
 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;
-- 
2.32.1 (Apple Git-133)



view thread (18+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected]
  Subject: Re: Raising the SCRAM iteration count
  In-Reply-To: <[email protected]>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox