Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 7ac955b

Browse files
committed
Allow SCRAM authentication, when pg_hba.conf says 'md5'.
If a user has a SCRAM verifier in pg_authid.rolpassword, there's no reason we cannot attempt to perform SCRAM authentication instead of MD5. The worst that can happen is that the client doesn't support SCRAM, and the authentication will fail. But previously, it would fail for sure, because we would not even try. SCRAM is strictly more secure than MD5, so there's no harm in trying it. This allows for a more graceful transition from MD5 passwords to SCRAM, as user passwords can be changed to SCRAM verifiers incrementally, without changing pg_hba.conf. Refactor the code in auth.c to support that better. Notably, we now have to look up the user's pg_authid entry before sending the password challenge, also when performing MD5 authentication. Also simplify the concept of a "doomed" authentication. Previously, if a user had a password, but it had expired, we still performed SCRAM authentication (but always returned error at the end) using the salt and iteration count from the expired password. Now we construct a fake salt, like we do when the user doesn't have a password or doesn't exist at all. That simplifies get_role_password(), and we can don't need to distinguish the "user has expired password", and "user does not exist" cases in auth.c. On second thoughts, also rename uaSASL to uaSCRAM. It refers to the mechanism specified in pg_hba.conf, and while we use SASL for SCRAM authentication at the protocol level, the mechanism should be called SCRAM, not SASL. As a comparison, we have uaLDAP, even though it looks like the plain 'password' authentication at the protocol level. Discussion: https://www.postgresql.org/message-id/6425.1489506016@sss.pgh.pa.us Reviewed-by: Michael Paquier
1 parent 7887453 commit 7ac955b

File tree

9 files changed

+214
-165
lines changed

9 files changed

+214
-165
lines changed

doc/src/sgml/client-auth.sgml

+18-19
Original file line numberDiff line numberDiff line change
@@ -412,23 +412,22 @@ hostnossl <replaceable>database</replaceable> <replaceable>user</replaceable>
412412
</varlistentry>
413413

414414
<varlistentry>
415-
<term><literal>md5</></term>
415+
<term><literal>scram</></term>
416416
<listitem>
417417
<para>
418-
Require the client to supply a double-MD5-hashed password for
419-
authentication.
420-
See <xref linkend="auth-password"> for details.
418+
Perform SCRAM-SHA-256 authentication to verify the user's
419+
password. See <xref linkend="auth-password"> for details.
421420
</para>
422421
</listitem>
423422
</varlistentry>
424423

425424
<varlistentry>
426-
<term><literal>scram</></term>
425+
<term><literal>md5</></term>
427426
<listitem>
428427
<para>
429-
Perform SCRAM-SHA-256 authentication to verify the user's
430-
password.
431-
See <xref linkend="auth-password"> for details.
428+
Perform SCRAM-SHA-256 or MD5 authentication to verify the
429+
user's password. See <xref linkend="auth-password">
430+
for details.
432431
</para>
433432
</listitem>
434433
</varlistentry>
@@ -689,13 +688,12 @@ host postgres all 192.168.12.10/32 scram
689688
# Allow any user from hosts in the example.com domain to connect to
690689
# any database if the user's password is correctly supplied.
691690
#
692-
# Most users use SCRAM authentication, but some users use older clients
693-
# that don't support SCRAM authentication, and need to be able to log
694-
# in using MD5 authentication. Such users are put in the @md5users
695-
# group, everyone else must use SCRAM.
691+
# Require SCRAM authentication for most users, but make an exception
692+
# for user 'mike', who uses an older client that doesn't support SCRAM
693+
# authentication.
696694
#
697695
# TYPE DATABASE USER ADDRESS METHOD
698-
host all @md5users .example.com md5
696+
host all mike .example.com md5
699697
host all all .example.com scram
700698

701699
# In the absence of preceding "host" lines, these two lines will
@@ -949,12 +947,13 @@ omicron bryanh guest1
949947
</para>
950948

951949
<para>
952-
In <literal>md5</>, the client sends a hash of a random challenge,
953-
generated by the server, and the password. It prevents password sniffing,
954-
but is less secure than <literal>scram</>, and provides no protection
955-
if an attacker manages to steal the password hash from the server.
956-
<literal>md5</> cannot be used with the <xref
957-
linkend="guc-db-user-namespace"> feature.
950+
<literal>md5</> allows falling back to a less secure challenge-response
951+
mechanism for those users with an MD5 hashed password.
952+
The fallback mechanism also prevents password sniffing, but provides no
953+
protection if an attacker manages to steal the password hash from the
954+
server, and it cannot be used with the <xref
955+
linkend="guc-db-user-namespace"> feature. For all other users,
956+
<literal>md5</> works the same as <literal>scram</>.
958957
</para>
959958

960959
<para>

src/backend/libpq/auth-scram.c

+58-46
Original file line numberDiff line numberDiff line change
@@ -130,79 +130,91 @@ static char *scram_MockSalt(const char *username);
130130
* after the beginning of the exchange with verifier data.
131131
*
132132
* 'username' is the provided by the client. 'shadow_pass' is the role's
133-
* password verifier, from pg_authid.rolpassword. If 'doomed' is true, the
134-
* authentication must fail, as if an incorrect password was given.
135-
* 'shadow_pass' may be NULL, when 'doomed' is set.
133+
* password verifier, from pg_authid.rolpassword. If 'shadow_pass' is NULL, we
134+
* still perform an authentication exchange, but it will fail, as if an
135+
* incorrect password was given.
136136
*/
137137
void *
138-
pg_be_scram_init(const char *username, const char *shadow_pass, bool doomed)
138+
pg_be_scram_init(const char *username, const char *shadow_pass)
139139
{
140140
scram_state *state;
141-
int password_type;
141+
bool got_verifier;
142142

143143
state = (scram_state *) palloc0(sizeof(scram_state));
144144
state->state = SCRAM_AUTH_INIT;
145145
state->username = username;
146146

147147
/*
148-
* Perform sanity checks on the provided password after catalog lookup.
149-
* The authentication is bound to fail if the lookup itself failed or if
150-
* the password stored is MD5-encrypted. Authentication is possible for
151-
* users with a valid plain password though.
148+
* Parse the stored password verifier.
152149
*/
150+
if (shadow_pass)
151+
{
152+
int password_type = get_password_type(shadow_pass);
153153

154-
if (shadow_pass == NULL || doomed)
155-
password_type = -1;
156-
else
157-
password_type = get_password_type(shadow_pass);
154+
if (password_type == PASSWORD_TYPE_SCRAM)
155+
{
156+
if (parse_scram_verifier(shadow_pass, &state->salt, &state->iterations,
157+
state->StoredKey, state->ServerKey))
158+
got_verifier = true;
159+
else
160+
{
161+
/*
162+
* The password looked like a SCRAM verifier, but could not be
163+
* parsed.
164+
*/
165+
elog(LOG, "invalid SCRAM verifier for user \"%s\"", username);
166+
got_verifier = false;
167+
}
168+
}
169+
else if (password_type == PASSWORD_TYPE_PLAINTEXT)
170+
{
171+
/*
172+
* The stored password is in plain format. Generate a fresh SCRAM
173+
* verifier from it, and proceed with that.
174+
*/
175+
char *verifier;
158176

159-
if (password_type == PASSWORD_TYPE_SCRAM)
160-
{
161-
if (!parse_scram_verifier(shadow_pass, &state->salt, &state->iterations,
162-
state->StoredKey, state->ServerKey))
177+
verifier = scram_build_verifier(username, shadow_pass, 0);
178+
179+
(void) parse_scram_verifier(verifier, &state->salt, &state->iterations,
180+
state->StoredKey, state->ServerKey);
181+
pfree(verifier);
182+
183+
got_verifier = true;
184+
}
185+
else
163186
{
164187
/*
165-
* The password looked like a SCRAM verifier, but could not be
166-
* parsed.
188+
* The user doesn't have SCRAM verifier, nor could we generate
189+
* one. (You cannot do SCRAM authentication with an MD5 hash.)
167190
*/
168-
elog(LOG, "invalid SCRAM verifier for user \"%s\"", username);
169-
doomed = true;
191+
state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."),
192+
state->username);
193+
got_verifier = false;
170194
}
171195
}
172-
else if (password_type == PASSWORD_TYPE_PLAINTEXT)
196+
else
173197
{
174-
char *verifier;
175-
176198
/*
177-
* The password provided is in plain format, in which case a fresh
178-
* SCRAM verifier can be generated and used for the rest of the
179-
* processing.
199+
* The caller requested us to perform a dummy authentication. This is
200+
* considered normal, since the caller requested it, so don't set log
201+
* detail.
180202
*/
181-
verifier = scram_build_verifier(username, shadow_pass, 0);
182-
183-
(void) parse_scram_verifier(verifier, &state->salt, &state->iterations,
184-
state->StoredKey, state->ServerKey);
185-
pfree(verifier);
203+
got_verifier = false;
186204
}
187-
else
188-
doomed = true;
189205

190-
if (doomed)
206+
/*
207+
* If the user did not have a valid SCRAM verifier, we still go through
208+
* the motions with a mock one, and fail as if the client supplied an
209+
* incorrect password. This is to avoid revealing information to an
210+
* attacker.
211+
*/
212+
if (!got_verifier)
191213
{
192-
/*
193-
* We don't have a valid SCRAM verifier, nor could we generate one, or
194-
* the caller requested us to perform a dummy authentication.
195-
*
196-
* The authentication is bound to fail, but to avoid revealing
197-
* information to the attacker, go through the motions with a fake
198-
* SCRAM verifier, and fail as if the password was incorrect.
199-
*/
200-
state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."),
201-
state->username);
202214
mock_scram_verifier(username, &state->salt, &state->iterations,
203215
state->StoredKey, state->ServerKey);
216+
state->doomed = true;
204217
}
205-
state->doomed = doomed;
206218

207219
return state;
208220
}

0 commit comments

Comments
 (0)