diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 481f93cea1b..71730cc52fe 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1132,6 +1132,26 @@ include_dir 'conf.d' + + scram_iterations (integer) + + scram_iterations configuration parameter + + + + + The number of computational iterations to be performed when encrypting + a password using SCRAM-SHA-256. The default is 4096. + 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. + + + + krb_server_keyfile (string) diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c index 4441e0d7745..9b286aa4d7f 100644 --- a/src/backend/libpq/auth-scram.c +++ b/src/backend/libpq/auth-scram.c @@ -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); diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 1c0583fe267..a60bd48499e 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -41,9 +41,11 @@ #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 diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index d06074b86f6..fc831565d90 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -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 diff --git a/src/common/scram-common.c b/src/common/scram-common.c index bd40d497a96..ef997ef6849 100644 --- a/src/common/scram-common.c +++ b/src/common/scram-common.c @@ -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, diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h index 0c75df55597..5ccff96ecee 100644 --- a/src/include/common/scram-common.h +++ b/src/include/common/scram-common.h @@ -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, diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h index b275e1e87e5..310bc365177 100644 --- a/src/include/libpq/scram.h +++ b/src/include/libpq/scram.h @@ -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; diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c index 277f72b280c..6b779ec7ffd 100644 --- a/src/interfaces/libpq/fe-auth-scram.c +++ b/src/interfaces/libpq/fe-auth-scram.c @@ -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); diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index 934e3f4f7ca..b0550e63324 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -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); } diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h index 1aa556ea2fa..124dd5d0313 100644 --- a/src/interfaces/libpq/fe-auth.h +++ b/src/interfaces/libpq/fe-auth.h @@ -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 */ diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 660775e0198..b71378d94c5 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -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; diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c index ec62550e385..a16bbf32ef5 100644 --- a/src/interfaces/libpq/fe-exec.c +++ b/src/interfaces/libpq/fe-exec.c @@ -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); + } } diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 10187c31b9a..88b9838d766 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -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; diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl index cba5d7d6487..00857fdae5a 100644 --- a/src/test/authentication/t/001_password.pl +++ b/src/test/authentication/t/001_password.pl @@ -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:/]); diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out index 7c84c9da337..84752317356 100644 --- a/src/test/regress/expected/password.out +++ b/src/test/regress/expected/password.out @@ -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:$:') 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:$: regress_passwd7 | SCRAM-SHA-256$4096:$: regress_passwd8 | SCRAM-SHA-256$4096:$: -(8 rows) + regress_passwd9 | SCRAM-SHA-256$1024:$: +(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 98f49916e5d..53e86b0b6ce 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'; +-- 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:$:') 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;