Make SCRAM iteration count configurable

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
This commit is contained in:
Daniel Gustafsson 2023-03-27 09:46:29 +02:00
parent c15631f0f5
commit b577743000
16 changed files with 94 additions and 10 deletions

View 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>

View 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);

View File

@ -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

View 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

View 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,

View 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,

View 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;

View 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);

View 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);
}

View 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 */

View 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;

View 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);
}
}

View 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;

View 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:/]);

View 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;

View 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;