From 9de52d4408f26a7758f76ddcff0a629522de4ca0 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson 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' + + scram_iteration_count (integer) + + scram_iteration_count configuration parameter + + + + + The number of computational iterations to be performed when generating + a SCRAM secret. The default is 15000. A higher + number of iterations will provide additional protection against + brute-force attacks on stored passwords but will make authentication + slower. + + + + krb_server_keyfile (string) 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:$: + regress_passwd3 | SCRAM-SHA-256$15000:$: 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:$:') 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:$: - regress_passwd4 | SCRAM-SHA-256$4096:$: + regress_passwd4 | SCRAM-SHA-256$15000:$: regress_passwd5 | md5e73a4b11df52a6068f8b39f90be36023 - regress_passwd6 | SCRAM-SHA-256$4096:$: - regress_passwd7 | SCRAM-SHA-256$4096:$: - regress_passwd8 | SCRAM-SHA-256$4096:$: -(8 rows) + regress_passwd6 | SCRAM-SHA-256$15000:$: + regress_passwd7 | SCRAM-SHA-256$15000:$: + regress_passwd8 | SCRAM-SHA-256$15000:$: + regress_passwd9 | SCRAM-SHA-256$99999:$: +(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:$:') 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)