scram and \password
Should the \password tool in psql inspect password_encryption and act on it
being 'scram'?
I didn't see this issue discussed, but the ability to search the archives
for backslashes is rather limited.
Cheers,
Jeff
On Sat, Mar 11, 2017 at 2:53 AM, Jeff Janes <jeff.janes@gmail.com> wrote:
Should the \password tool in psql inspect password_encryption and act on it
being 'scram'?
Not sure if it is wise to change the default fot this release.
I didn't see this issue discussed, but the ability to search the archives
for backslashes is rather limited.
I'll save you some time: it has not been discussed. Nor has
PQencryptPassword been mentioned. Changing to SCRAM is not that
complicated, just call scram_build_verifier() and you are good to go.
Instead of changing the default, I think that we should take this
occasion to rename PQencryptPassword to something like
PQhashPassword(), and extend it with a method argument to support both
md5 and scram. PQencryptPassword could also be marked as deprecated,
or let as-is for some time. For \password, we could have another
meta-command but that sounds grotty, or just extend \password with a
--method argument. Switching the default could happen in another
release.
A patch among those lines would be a simple, do people feel that this
should be part of PG 10?
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/10/2017 02:43 PM, Michael Paquier wrote:
On Sat, Mar 11, 2017 at 2:53 AM, Jeff Janes <jeff.janes@gmail.com> wrote:
Should the \password tool in psql inspect password_encryption and act on it
being 'scram'?Not sure if it is wise to change the default fot this release.
I didn't see this issue discussed, but the ability to search the archives
for backslashes is rather limited.I'll save you some time: it has not been discussed. Nor has
PQencryptPassword been mentioned. Changing to SCRAM is not that
complicated, just call scram_build_verifier() and you are good to go.Instead of changing the default, I think that we should take this
occasion to rename PQencryptPassword to something like
PQhashPassword(), and extend it with a method argument to support both
md5 and scram. PQencryptPassword could also be marked as deprecated,
or let as-is for some time. For \password, we could have another
meta-command but that sounds grotty, or just extend \password with a
--method argument. Switching the default could happen in another
release.A patch among those lines would be a simple, do people feel that this
should be part of PG 10?
Seems like it should work in an analogous way to CREATE/ALTER ROLE.
According to the docs:
8<----
ENCRYPTED
UNENCRYPTED
These key words control whether the password is stored encrypted in
the system catalogs. (If neither is specified, the default behavior is
determined by the configuration parameter password_encryption.) If the
presented password string is already in MD5-encrypted or SCRAM-encrypted
format, then it is stored encrypted as-is, regardless of whether
ENCRYPTED or UNENCRYPTED is specified (since the system cannot decrypt
the specified encrypted password string). This allows reloading of
encrypted passwords during dump/restore.
8<----
So if the password is not already set, \password uses
password_encryption to determine which format to use, and if the
password is already set, then the current method is assumed.
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
On Sun, Mar 12, 2017 at 5:35 AM, Joe Conway <mail@joeconway.com> wrote:
On 03/10/2017 02:43 PM, Michael Paquier wrote:
Instead of changing the default, I think that we should take this
occasion to rename PQencryptPassword to something like
PQhashPassword(), and extend it with a method argument to support both
md5 and scram. PQencryptPassword could also be marked as deprecated,
or let as-is for some time. For \password, we could have another
meta-command but that sounds grotty, or just extend \password with a
--method argument. Switching the default could happen in another
release.A patch among those lines would be a simple, do people feel that this
should be part of PG 10?Seems like it should work in an analogous way to CREATE/ALTER ROLE.
According to the docs:8<----
ENCRYPTED
UNENCRYPTEDThese key words control whether the password is stored encrypted in
the system catalogs. (If neither is specified, the default behavior is
determined by the configuration parameter password_encryption.) If the
presented password string is already in MD5-encrypted or SCRAM-encrypted
format, then it is stored encrypted as-is, regardless of whether
ENCRYPTED or UNENCRYPTED is specified (since the system cannot decrypt
the specified encrypted password string). This allows reloading of
encrypted passwords during dump/restore.
8<----So if the password is not already set, \password uses
password_encryption to determine which format to use, and if the
password is already set, then the current method is assumed.
Yeah, the problem here being that this routine does not need a live
connection to work, and we surely don't want to make that mandatory,
that's why I am suggesting something like the above. Another approach
would be to switch to SCRAM once password_encryption does this switch
as well... There is no perfect scenario here.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/11/2017 02:21 PM, Michael Paquier wrote:
On Sun, Mar 12, 2017 at 5:35 AM, Joe Conway <mail@joeconway.com> wrote:
So if the password is not already set, \password uses
password_encryption to determine which format to use, and if the
password is already set, then the current method is assumed.Yeah, the problem here being that this routine does not need a live
connection to work, and we surely don't want to make that mandatory,
that's why I am suggesting something like the above. Another approach
would be to switch to SCRAM once password_encryption does this switch
as well... There is no perfect scenario here.
You might extend PQencryptPassword() to take a method. Or create a new
function that does? Certainly psql has a connection available to run the
ALTER ROLE command that it crafts.
I guess a related problem might be, do we have a SQL visible way to
determine what method is used by the current password for a given role?
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
On Sun, Mar 12, 2017 at 8:04 AM, Joe Conway <mail@joeconway.com> wrote:
On 03/11/2017 02:21 PM, Michael Paquier wrote:
On Sun, Mar 12, 2017 at 5:35 AM, Joe Conway <mail@joeconway.com> wrote:
So if the password is not already set, \password uses
password_encryption to determine which format to use, and if the
password is already set, then the current method is assumed.Yeah, the problem here being that this routine does not need a live
connection to work, and we surely don't want to make that mandatory,
that's why I am suggesting something like the above. Another approach
would be to switch to SCRAM once password_encryption does this switch
as well... There is no perfect scenario here.You might extend PQencryptPassword() to take a method. Or create a new
function that does? Certainly psql has a connection available to run the
ALTER ROLE command that it crafts.
Yeah but it can be called as well while the application is calling
PQgetResult() and still looping until it gets a NULL result. Not sure
if this is a use-case to worry about, but sending a query to the
server in PQencryptPassword() could as well break some applications.
PQencryptPassword() is used for CREATE/ALTER ROLE commands, so
actually wouldn't it make sense to just switch PQencryptPassword to
handle SCRAM if at some point we decide to switch the default from md5
to scram? So many questions.
I guess a related problem might be, do we have a SQL visible way to
determine what method is used by the current password for a given role?
Nope. We are simply looking at a function doing a lookup at pg_authid
and then use get_password_type() to check which type of verifier is
used... Or have the type of verifier as a new column of pg_authid,
information that could be made visible to any users with column
privileges.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/11/2017 03:15 PM, Michael Paquier wrote:
Yeah but it can be called as well while the application is calling
PQgetResult() and still looping until it gets a NULL result. Not sure
if this is a use-case to worry about, but sending a query to the
server in PQencryptPassword() could as well break some applications.
I was suggesting sending the query outside of PQencryptPassword() in
order to determine what method should be passed as a new argument to
PQencryptPassword().
PQencryptPassword() is used for CREATE/ALTER ROLE commands, so
actually wouldn't it make sense to just switch PQencryptPassword to
handle SCRAM if at some point we decide to switch the default from md5
to scram? So many questions.
As long as we support more than one method it would seem to me we need a
way to determine which one we want to use and not only default it, don't
we? Apologies if this has already been discussed -- I was not able to
follow the lengthy threads on SCRAM in any detail.
I guess a related problem might be, do we have a SQL visible way to
determine what method is used by the current password for a given role?Nope. We are simply looking at a function doing a lookup at pg_authid
and then use get_password_type() to check which type of verifier is
used... Or have the type of verifier as a new column of pg_authid,
information that could be made visible to any users with column
privileges.
What happens if the user does not have privs for pg_authid? E.g. if I
want to change my own password what happens if the default is one
method, and my password uses the other -- now I cannot change my own
password using \password?
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
On Sun, Mar 12, 2017 at 9:10 AM, Joe Conway <mail@joeconway.com> wrote:
On 03/11/2017 03:15 PM, Michael Paquier wrote:
Yeah but it can be called as well while the application is calling
PQgetResult() and still looping until it gets a NULL result. Not sure
if this is a use-case to worry about, but sending a query to the
server in PQencryptPassword() could as well break some applications.I was suggesting sending the query outside of PQencryptPassword() in
order to determine what method should be passed as a new argument to
PQencryptPassword().
Why not. Our thoughts don't overlap, I thought about having
PQencryptPassword() call itself the server for the value of
password_encryption, and force the type depending on what the server
answers.
PQencryptPassword() is used for CREATE/ALTER ROLE commands, so
actually wouldn't it make sense to just switch PQencryptPassword to
handle SCRAM if at some point we decide to switch the default from md5
to scram? So many questions.As long as we support more than one method it would seem to me we need a
way to determine which one we want to use and not only default it, don't
we? Apologies if this has already been discussed -- I was not able to
follow the lengthy threads on SCRAM in any detail.
Definitely, the most simple solution would be just to extend
PQencryptPassword() with a method value, to allow a user to do what he
wants...
I guess a related problem might be, do we have a SQL visible way to
determine what method is used by the current password for a given role?Nope. We are simply looking at a function doing a lookup at pg_authid
and then use get_password_type() to check which type of verifier is
used... Or have the type of verifier as a new column of pg_authid,
information that could be made visible to any users with column
privileges.What happens if the user does not have privs for pg_authid? E.g. if I
want to change my own password what happens if the default is one
method, and my password uses the other -- now I cannot change my own
password using \password?
You can now. However it would be a problem for a user having a SCRAM
verifier using an application that changes the password with
PQencryptPassword() as it would change it back to MD5 on an update.
Having a RLS on pg_authid to allow a user to look at its own password
type is an idea. With multiple verifier types per role such class of
bugs can be also completely discarded.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/11/2017 11:07 PM, Michael Paquier wrote:
Having a RLS on pg_authid to allow a user to look at its own password
type is an idea.
Given that that is not likely at this stage of the dev cycle, what about
a special purpose SQL function that returns the password type for the
current user? Or is it a security issue of some sort to allow a user to
know their own password type?
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
On Fri, Mar 10, 2017 at 5:43 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:
On Sat, Mar 11, 2017 at 2:53 AM, Jeff Janes <jeff.janes@gmail.com> wrote:
Should the \password tool in psql inspect password_encryption and act on it
being 'scram'?Not sure if it is wise to change the default fot this release.
Seems like an odd way to phrase it. Aren't we talking about making a
feature that worked in previous releases continue to work?
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Mar 13, 2017 at 12:48 AM, Joe Conway <mail@joeconway.com> wrote:
On 03/11/2017 11:07 PM, Michael Paquier wrote:
Having a RLS on pg_authid to allow a user to look at its own password
type is an idea.Given that that is not likely at this stage of the dev cycle, what about
a special purpose SQL function that returns the password type for the
current user?
OK, so what about doing the following then:
1) Create pg_role_password_type(oid), taking a role OID in input and
returning the type.
2) Extend PQencryptPassword with a method, and document. Plaintext is forbidden.
3) Extend \password in psql with a similar -method=scram|md5 argument,
and forbid the use of "plain" format.
After thinking about it, extending PQencryptPassword() would lead to
future breakage, which makes it clear to not fall into the trap of
having password_encryption set to scram in the server's as well as in
pg_hba.conf and PQencryptPassword() enforcing things to md5.
Or is it a security issue of some sort to allow a user to
know their own password type?
Don't think so. Any user has access to the value of
password_encryption. So one can guess what will be the format of a
password hashed.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Mar 13, 2017 at 9:15 AM, Robert Haas <robertmhaas@gmail.com> wrote:
On Fri, Mar 10, 2017 at 5:43 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:On Sat, Mar 11, 2017 at 2:53 AM, Jeff Janes <jeff.janes@gmail.com> wrote:
Should the \password tool in psql inspect password_encryption and act on it
being 'scram'?Not sure if it is wise to change the default fot this release.
Seems like an odd way to phrase it. Aren't we talking about making a
feature that worked in previous releases continue to work?
Considering how fresh scram is, it seems clear to me that we do not
want to just switch the default values of password_encryption, the
default behaviors of PQencryptPassword() and \password only to scram,
but have something else. Actually if we change nothing for default
deployments of Postgres using md5, PQencryptPassword() and \password
would still work properly.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Mar 10, 2017 at 2:43 PM, Michael Paquier <michael.paquier@gmail.com>
wrote:
On Sat, Mar 11, 2017 at 2:53 AM, Jeff Janes <jeff.janes@gmail.com> wrote:
Should the \password tool in psql inspect password_encryption and act on
it
being 'scram'?
Not sure if it is wise to change the default fot this release.
I'm not proposing that we change the default value of password_encryption,
only that \password respect whatever value it currently finds there. But
after thinking about it a bit more, I reached the same conclusion that Joe
did, that it should use the same hashing method as the current password
does, and only consult password_encryption if there is no password
currently set.
A patch among those lines would be a simple, do people feel that this
should be part of PG 10?
I think it is pretty important to have some way of setting the password
that doesn't risk it ending up in the server log file, or .psql_history, or
having someone shoulder-surf it.
Cheers,
Jeff
On 03/12/2017 08:10 PM, Michael Paquier wrote:
OK, so what about doing the following then:
1) Create pg_role_password_type(oid), taking a role OID in input and
returning the type.
That version would make sense for administrative use. I still think we
might want a version of this that takes no argument, works on the
current_user, and is executable by anyone.
2) Extend PQencryptPassword with a method, and document. Plaintext is forbidden.
Check, although if "plain" were allowed as a method for the sake of
consistency/completeness the function could just immediately return the
argument.
3) Extend \password in psql with a similar -method=scram|md5 argument,
and forbid the use of "plain" format.
Not sure why we would forbid "plain" here unless we remove it entirely
elsewhere.
After thinking about it, extending PQencryptPassword() would lead to
future breakage, which makes it clear to not fall into the trap of
having password_encryption set to scram in the server's as well as in
pg_hba.conf and PQencryptPassword() enforcing things to md5.
I'm not grokking this statement
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
On Tue, Mar 14, 2017 at 9:54 AM, Joe Conway <mail@joeconway.com> wrote:
On 03/12/2017 08:10 PM, Michael Paquier wrote:
OK, so what about doing the following then:
1) Create pg_role_password_type(oid), taking a role OID in input and
returning the type.That version would make sense for administrative use. I still think we
might want a version of this that takes no argument, works on the
current_user, and is executable by anyone.
Sure.
2) Extend PQencryptPassword with a method, and document. Plaintext is forbidden.
Check, although if "plain" were allowed as a method for the sake of
consistency/completeness the function could just immediately return the
argument.3) Extend \password in psql with a similar -method=scram|md5 argument,
and forbid the use of "plain" format.Not sure why we would forbid "plain" here unless we remove it entirely
elsewhere.
OK. I don't mind about those two, as long as documentation is clear
enough about what using plain leads to.
After thinking about it, extending PQencryptPassword() would lead to
future breakage, which makes it clear to not fall into the trap of
having password_encryption set to scram in the server's as well as in
pg_hba.conf and PQencryptPassword() enforcing things to md5.I'm not grokking this statement
To grok: "To understand profoundly through intuition or empathy.".
Learning a new thing everyday.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sun, Mar 12, 2017 at 11:14 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:
On Mon, Mar 13, 2017 at 9:15 AM, Robert Haas <robertmhaas@gmail.com> wrote:
On Fri, Mar 10, 2017 at 5:43 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:On Sat, Mar 11, 2017 at 2:53 AM, Jeff Janes <jeff.janes@gmail.com> wrote:
Should the \password tool in psql inspect password_encryption and act on it
being 'scram'?Not sure if it is wise to change the default fot this release.
Seems like an odd way to phrase it. Aren't we talking about making a
feature that worked in previous releases continue to work?Considering how fresh scram is, it seems clear to me that we do not
want to just switch the default values of password_encryption, the
default behaviors of PQencryptPassword() and \password only to scram,
but have something else. Actually if we change nothing for default
deployments of Postgres using md5, PQencryptPassword() and \password
would still work properly.
I'm not talking about changing the default, just having it be possible
to use \password with the new system as it was with the old, whatever
exactly we think that means.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas <robertmhaas@gmail.com> writes:
I'm not talking about changing the default, just having it be possible
to use \password with the new system as it was with the old, whatever
exactly we think that means.
Seems to me the intended behavior of \password is to use the best
available practice. So my guess is that it ought to use SCRAM when
talking to a >= 10.0 server. What the previous password was ought
to be irrelevant, even if it could find that out which it shouldn't
be able to IMO.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Mar 14, 2017 at 11:47 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
I'm not talking about changing the default, just having it be possible
to use \password with the new system as it was with the old, whatever
exactly we think that means.
I think that this means looking at password_encryption within
PQencryptPassword(), something that could silently break some
applications. That's why with Joe we are mentioning upthread to extend
PQencryptPassword() with a hashing method, and have a function to
allow retrieval of the password type for a given user.
Seems to me the intended behavior of \password is to use the best
available practice. So my guess is that it ought to use SCRAM when
talking to a >= 10.0 server. What the previous password was ought
to be irrelevant, even if it could find that out which it shouldn't
be able to IMO.
And in a release or two? SCRAM being a fresh feature, switching the
hashing now is not much a conservative approach.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Michael Paquier <michael.paquier@gmail.com> writes:
On Tue, Mar 14, 2017 at 11:47 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Seems to me the intended behavior of \password is to use the best
available practice. So my guess is that it ought to use SCRAM when
talking to a >= 10.0 server. What the previous password was ought
to be irrelevant, even if it could find that out which it shouldn't
be able to IMO.
And in a release or two? SCRAM being a fresh feature, switching the
hashing now is not much a conservative approach.
If some other practice becomes better in v12, then we teach it about that
one. It's not like psql hasn't got many other server-version-dependent
behaviors.
Alternatively, if what you mean by that is you don't trust SCRAM at all,
maybe we'd better revert the feature as not being ready for prime time.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Mar 14, 2017 at 12:34 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Michael Paquier <michael.paquier@gmail.com> writes:
If some other practice becomes better in v12, then we teach it about that
one. It's not like psql hasn't got many other server-version-dependent
behaviors.
Okay. Looking at the code, scram_build_verifier is only available on
the backend side, so if we need to rethink it a bit to be able to have
libpq build a SCRAM verifier:
- The allocation of the verifier buffer needs to happen outside it,
and the caller needs to provide an allocated buffer. Its size had
better be calculated a macro.
- The routine needs to be moved to scram-common.c.
- a copy of hex_encode, say pg_hex_encode added in its own file in
src/common/, needs to be provided. That's 6 lines of code, we could
just have that in scram-common.c.
Does that sound correct?
Alternatively, if what you mean by that is you don't trust SCRAM at all,
maybe we'd better revert the feature as not being ready for prime time.
FWIW, I am confident about the code. Based on years watching this
mailing list, switching long-term behaviors is usually done in a less
faster pace, that's the only reason on which my opinion is based.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/14/2017 04:47 AM, Tom Lane wrote:
Robert Haas <robertmhaas@gmail.com> writes:
I'm not talking about changing the default, just having it be possible
to use \password with the new system as it was with the old, whatever
exactly we think that means.Seems to me the intended behavior of \password is to use the best
available practice. So my guess is that it ought to use SCRAM when
talking to a >= 10.0 server. What the previous password was ought
to be irrelevant, even if it could find that out which it shouldn't
be able to IMO.
If the server isn't set up to do SCRAM authentication, i.e. there are no
"scram" entries in pg_hba.conf, and you set yourself a SCRAM verifier,
you have just locked yourself out of the system. I think that's a
non-starter. There needs to be some more intelligence in the decision.
It would be a lot more sensible, if there was a way to specify in
pg_hba.conf, "scram-or-md5". We punted on that for PostgreSQL 10, but
perhaps we should try to cram that in, after all.
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/14/2017 03:15 AM, Heikki Linnakangas wrote:
On 03/14/2017 04:47 AM, Tom Lane wrote:
Robert Haas <robertmhaas@gmail.com> writes:
I'm not talking about changing the default, just having it be possible
to use \password with the new system as it was with the old, whatever
exactly we think that means.Seems to me the intended behavior of \password is to use the best
available practice. So my guess is that it ought to use SCRAM when
talking to a >= 10.0 server. What the previous password was ought
to be irrelevant, even if it could find that out which it shouldn't
be able to IMO.If the server isn't set up to do SCRAM authentication, i.e. there are no
"scram" entries in pg_hba.conf, and you set yourself a SCRAM verifier,
you have just locked yourself out of the system. I think that's a
non-starter. There needs to be some more intelligence in the decision.
Yes, this was exactly my concern.
It would be a lot more sensible, if there was a way to specify in
pg_hba.conf, "scram-or-md5". We punted on that for PostgreSQL 10, but
perhaps we should try to cram that in, after all.
I was also thinking about that. Basically a primary method and a
fallback. If that were the case, a gradual transition could happen, and
if we want \password to enforce best practice it would be ok.
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
Joe Conway <mail@joeconway.com> writes:
On 03/14/2017 03:15 AM, Heikki Linnakangas wrote:
If the server isn't set up to do SCRAM authentication, i.e. there are no
"scram" entries in pg_hba.conf, and you set yourself a SCRAM verifier,
you have just locked yourself out of the system. I think that's a
non-starter. There needs to be some more intelligence in the decision.
Yes, this was exactly my concern.
This seems like a serious usability fail.
It would be a lot more sensible, if there was a way to specify in
pg_hba.conf, "scram-or-md5". We punted on that for PostgreSQL 10, but
perhaps we should try to cram that in, after all.
I was also thinking about that. Basically a primary method and a
fallback. If that were the case, a gradual transition could happen, and
if we want \password to enforce best practice it would be ok.
Why exactly would anyone want "md5 only"? I should think that "scram
only" is a sensible pg_hba setting, if the DBA feels that md5 is too
insecure, but I do not see the point of "md5 only" in 2017. I think
we should just start interpreting that as "md5 or better".
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/14/2017 08:40 AM, Tom Lane wrote:
Joe Conway <mail@joeconway.com> writes:
On 03/14/2017 03:15 AM, Heikki Linnakangas wrote:
It would be a lot more sensible, if there was a way to specify in
pg_hba.conf, "scram-or-md5". We punted on that for PostgreSQL 10, but
perhaps we should try to cram that in, after all.I was also thinking about that. Basically a primary method and a
fallback. If that were the case, a gradual transition could happen, and
if we want \password to enforce best practice it would be ok.Why exactly would anyone want "md5 only"? I should think that "scram
only" is a sensible pg_hba setting, if the DBA feels that md5 is too
insecure, but I do not see the point of "md5 only" in 2017. I think
we should just start interpreting that as "md5 or better".
That certainly would work for me.
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
On Tue, Mar 14, 2017 at 3:15 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 03/14/2017 04:47 AM, Tom Lane wrote:
Robert Haas <robertmhaas@gmail.com> writes:
I'm not talking about changing the default, just having it be possible
to use \password with the new system as it was with the old, whatever
exactly we think that means.Seems to me the intended behavior of \password is to use the best
available practice. So my guess is that it ought to use SCRAM when
talking to a >= 10.0 server. What the previous password was ought
to be irrelevant, even if it could find that out which it shouldn't
be able to IMO.If the server isn't set up to do SCRAM authentication, i.e. there are no
"scram" entries in pg_hba.conf,
The method is not part of the pg_hba matching algorithm, so either the
first match is scram, or it isn't. There is no fallback to later entries,
as I understand it.
and you set yourself a SCRAM verifier, you have just locked yourself out
of the system. I think that's a non-starter. There needs to be some more
intelligence in the decision.
Right. And you can lock yourself out of the system going the other way, as
well, setting a md5 password when scram is in pg_hba. Which is how I ended
up starting this thread.
It would be a lot more sensible, if there was a way to specify in
pg_hba.conf, "scram-or-md5". We punted on that for PostgreSQL 10, but
perhaps we should try to cram that in, after all.
So the backend for the incipient connection would consult the catalog
pg_authid before responding back to the client with an authentication
method, as opposed to merely pulling it out of pg_hba?
Cheers,
Jeff
On Tue, Mar 14, 2017 at 8:40 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Joe Conway <mail@joeconway.com> writes:
I was also thinking about that. Basically a primary method and a
fallback. If that were the case, a gradual transition could happen, and
if we want \password to enforce best practice it would be ok.Why exactly would anyone want "md5 only"? I should think that "scram
only" is a sensible pg_hba setting, if the DBA feels that md5 is too
insecure, but I do not see the point of "md5 only" in 2017. I think
we should just start interpreting that as "md5 or better".
Without md5-only, a user who uses \password to change their password from a
newer client would lock themselves out of connecting again from older
clients. As a conscious decision (either of the DBA or the user) that
would be OK, but to have it happen by default would be unfortunate.
Cheers,
Jeff
Jeff Janes <jeff.janes@gmail.com> writes:
On Tue, Mar 14, 2017 at 8:40 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Why exactly would anyone want "md5 only"? I should think that "scram
only" is a sensible pg_hba setting, if the DBA feels that md5 is too
insecure, but I do not see the point of "md5 only" in 2017. I think
we should just start interpreting that as "md5 or better".
Without md5-only, a user who uses \password to change their password from a
newer client would lock themselves out of connecting again from older
clients. As a conscious decision (either of the DBA or the user) that
would be OK, but to have it happen by default would be unfortunate.
That's a point, but what it implies is that \password needs some input
from the user about whether to generate a SCRAM or MD5-hashed password.
It would be a fatal error to try to drive that off the auth method
that had been used for the current connection, even if \password had a
way to find that out. By definition, your concern is about clients
other than the current one, which might well be coming in from other
addresses and getting challenges based on other pg_hba entries. So
you can't say that "I came in on a SCRAM connection" is sufficient
reason to generate a SCRAM password.
In short, I don't think that argument refutes my position that "md5"
in pg_hba.conf should be understood as allowing SCRAM passwords too.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Mar 14, 2017 at 5:14 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Without md5-only, a user who uses \password to change their password from a
newer client would lock themselves out of connecting again from older
clients. As a conscious decision (either of the DBA or the user) that
would be OK, but to have it happen by default would be unfortunate.That's a point, but what it implies is that \password needs some input
from the user about whether to generate a SCRAM or MD5-hashed password.
It would be a fatal error to try to drive that off the auth method
that had been used for the current connection, even if \password had a
way to find that out. By definition, your concern is about clients
other than the current one, which might well be coming in from other
addresses and getting challenges based on other pg_hba entries. So
you can't say that "I came in on a SCRAM connection" is sufficient
reason to generate a SCRAM password.
To some extent that seems like a question of system policy. Either
the DBA wants users to use SCRAM passwords, or the DBA wants users to
use MD5 passwords, or either is permissible. In the last case, the
user can do what they like, but it seems like a fairly bad idea from a
user perspective to let the user configure a password using a system
that will lock them out. We shouldn't assume the user even has any
knowledge of what's in pg_hba.conf, or that they would know what those
contents meant if they had them. There ought to be something like a
PGC_SUSER GUC that sets the kinds of password verifiers that a user is
allowed to configure, and maybe \password should default to the first
one in the list (but possibly be overridable?).
In short, I don't think that argument refutes my position that "md5"
in pg_hba.conf should be understood as allowing SCRAM passwords too.
I'm not sure that's a bad idea, but my first reaction is not to like
it. md5 is a funny spelling of md5-or-scram.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Mar 15, 2017 at 6:14 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Jeff Janes <jeff.janes@gmail.com> writes:
On Tue, Mar 14, 2017 at 8:40 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Why exactly would anyone want "md5 only"? I should think that "scram
only" is a sensible pg_hba setting, if the DBA feels that md5 is too
insecure, but I do not see the point of "md5 only" in 2017. I think
we should just start interpreting that as "md5 or better".Without md5-only, a user who uses \password to change their password from a
newer client would lock themselves out of connecting again from older
clients. As a conscious decision (either of the DBA or the user) that
would be OK, but to have it happen by default would be unfortunate.That's a point, but what it implies is that \password needs some input
from the user about whether to generate a SCRAM or MD5-hashed password.
It would be a fatal error to try to drive that off the auth method
that had been used for the current connection, even if \password had a
way to find that out. By definition, your concern is about clients
other than the current one, which might well be coming in from other
addresses and getting challenges based on other pg_hba entries. So
you can't say that "I came in on a SCRAM connection" is sufficient
reason to generate a SCRAM password.In short, I don't think that argument refutes my position that "md5"
in pg_hba.conf should be understood as allowing SCRAM passwords too.
I have been hacking my way through this thing, and making
scram_build_verifier is requiring a bit more refactoring than I
thought first:
- stored and server keys are hex-encoded using a backend-only routine.
I think that those should be instead base64-encoded using what has
already been moved in src/common/.
- Callers of scram_build_verifier() need to allocate by themselves the
verifier, and feed it to the function similarly to MD5.
- Frontend-side random generation function is needed, so I have moved
pg_frontend_random() into its own file in src/common/.
Attached are four patches:
- 0001: Switch server and stored keys to be base64-encoded.
- 0002: Move pg_frontend_random() to src/common/
- 0003: Move scram_build_verifier() to src/common/
- 0004: Extend PQencryptPassword with a method argument, able to
handle SCRAM, MD5 and cleartext.
I have not yet done the psql portion with \password -method (too
tired), and in 0004 all the calls of PQencryptPassword use "scram" for
the purpose of the demonstration. Even if PQencryptPassword is not
extended at the end, patches 0001~0003 are necessary anyway.
--
Michael
Attachments:
0004-Extend-PQencryptPassword-with-a-hashing-method.patchapplication/octet-stream; name=0004-Extend-PQencryptPassword-with-a-hashing-method.patchDownload
From 8849fb8d0ef824377f6af36f33757060162b7fa7 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 16 Mar 2017 00:25:51 +0900
Subject: [PATCH 4/4] Extend PQencryptPassword with a hashing method
This extra argument can use the following values when hashing the
password:
- scram, for SCRAM-SHA-256 hashing.
- md5, for MD5 hashing.
- plain, for cleartext.
---
doc/src/sgml/libpq.sgml | 6 ++++-
src/bin/psql/command.c | 2 +-
src/bin/scripts/createuser.c | 3 ++-
src/interfaces/libpq/fe-auth.c | 49 +++++++++++++++++++++++++++++++----------
src/interfaces/libpq/libpq-fe.h | 3 ++-
5 files changed, 47 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 4bc5bf3192..fc1aa4b5e5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -5887,7 +5887,8 @@ void PQconninfoFree(PQconninfoOption *connOptions);
<para>
Prepares the encrypted form of a <productname>PostgreSQL</> password.
<synopsis>
-char * PQencryptPassword(const char *passwd, const char *user);
+char * PQencryptPassword(const char *passwd, const char *user,
+ const char *method);
</synopsis>
This function is intended to be used by client applications that
wish to send commands like <literal>ALTER USER joe PASSWORD
@@ -5901,6 +5902,9 @@ char * PQencryptPassword(const char *passwd, const char *user);
memory. The caller can assume the string doesn't contain any
special characters that would require escaping. Use
<function>PQfreemem</> to free the result when done with it.
+ The encryption method of the password can be specified as
+ <literal>md5</> for hashing with MD5, <literal>scram</> for
+ hashing with SCRAM-SHA-256 and <literal>plain</> for cleartext.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 4f4a0aa9bd..c100256dea 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1135,7 +1135,7 @@ exec_command(const char *cmd,
else
user = PQuser(pset.db);
- encrypted_password = PQencryptPassword(pw1, user);
+ encrypted_password = PQencryptPassword(pw1, user, "scram");
if (!encrypted_password)
{
diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c
index 3d74797a8f..42e1dc8650 100644
--- a/src/bin/scripts/createuser.c
+++ b/src/bin/scripts/createuser.c
@@ -275,7 +275,8 @@ main(int argc, char *argv[])
char *encrypted_password;
encrypted_password = PQencryptPassword(newpassword,
- newuser);
+ newuser,
+ "scram");
if (!encrypted_password)
{
fprintf(stderr, _("Password encryption failed.\n"));
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 5fe7e565a0..c94fb6127f 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -39,6 +39,7 @@
#endif
#include "common/md5.h"
+#include "common/scram-common.h"
#include "libpq-fe.h"
#include "libpq/scram.h"
#include "fe-auth.h"
@@ -919,27 +920,51 @@ pg_fe_getauthname(PQExpBuffer errorMessage)
* be dependent on low-level details like whether the encryption is MD5
* or something else.
*
- * Arguments are the cleartext password, and the SQL name of the user it
- * is for.
+ * Arguments are the cleartext password, the SQL name of the user it
+ * is for, and the name of password hashing method:
+ * - "scram", to hash password using SCRAM-SHA-256.
+ * - "md5", to hash password using MD5.
+ * - "plain", to get a cleartext value of password.
*
- * Return value is a malloc'd string, or NULL if out-of-memory. The client
- * may assume the string doesn't contain any special characters that would
- * require escaping.
+ * Return value is a malloc'd string, or NULL if out-of-memory or in
+ * the event of an error. The client may assume the string doesn't
+ * contain any special characters that would require escaping.
*/
char *
-PQencryptPassword(const char *passwd, const char *user)
+PQencryptPassword(const char *passwd, const char *user, const char *method)
{
char *crypt_pwd;
- crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
- if (!crypt_pwd)
- return NULL;
+ if (strcmp(method, "md5") == 0)
+ {
+ crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
+ if (!crypt_pwd)
+ return NULL;
- if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ {
+ free(crypt_pwd);
+ return NULL;
+ }
+ }
+ else if (strcmp(method, "scram") == 0)
{
- free(crypt_pwd);
- return NULL;
+ crypt_pwd = malloc(SCRAM_VERIFIER_LEN + 1);
+ if (!crypt_pwd)
+ return NULL;
+
+ if (!scram_build_verifier(user, passwd, 0, crypt_pwd))
+ {
+ free(crypt_pwd);
+ return NULL;
+ }
}
+ else if (strcmp(method, "plain") == 0)
+ {
+ crypt_pwd = strdup(passwd);
+ }
+ else
+ return NULL;
return crypt_pwd;
}
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 635af5b50e..c312dd0152 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -596,7 +596,8 @@ extern int PQenv2encoding(void);
/* === in fe-auth.c === */
-extern char *PQencryptPassword(const char *passwd, const char *user);
+extern char *PQencryptPassword(const char *passwd, const char *user,
+ const char *method);
/* === in encnames.c === */
--
2.12.0
0001-Use-base64-based-encoding-for-stored-and-server-keys.patchapplication/octet-stream; name=0001-Use-base64-based-encoding-for-stored-and-server-keys.patchDownload
From 6bc12ddad069e8320f5533435695b35030399c86 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 16 Mar 2017 00:35:40 +0900
Subject: [PATCH 1/4] Use base64-based encoding for stored and server keys in
SCRAM verifiers
In order to be able to generate a SCRAM verifier even for frontends, let's
simplify the tools used to generate it and switch all the elements of the
verifiers to be base64-encoded using the routines already in place in
src/common/.
---
doc/src/sgml/catalogs.sgml | 2 +-
src/backend/libpq/auth-scram.c | 30 +++++++++++++++++-------------
src/test/regress/expected/password.out | 8 ++++----
src/test/regress/sql/password.sql | 8 ++++----
4 files changed, 26 insertions(+), 22 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2c2da2ad8a..737c6f1a48 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1361,7 +1361,7 @@
identify the password as a SCRAM-SHA-256 verifier. The second field is a
salt, Base64-encoded, and the third field is the number of iterations used
to generate the password. The fourth field and fifth field are the stored
- key and server key, respectively, in hexadecimal format. A password that
+ key and server key, respectively, in Base64 format. A password that
does not follow either of those formats is assumed to be unencrypted.
</para>
</sect1>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 9f78e57aae..29e2965883 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -327,8 +327,8 @@ scram_build_verifier(const char *username, const char *password,
int iterations)
{
uint8 keybuf[SCRAM_KEY_LEN + 1];
- char storedkey_hex[SCRAM_KEY_LEN * 2 + 1];
- char serverkey_hex[SCRAM_KEY_LEN * 2 + 1];
+ char *encoded_storedkey;
+ char *encoded_serverkey;
char salt[SCRAM_SALT_LEN];
char *encoded_salt;
int encoded_len;
@@ -348,20 +348,25 @@ scram_build_verifier(const char *username, const char *password,
encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt);
encoded_salt[encoded_len] = '\0';
- /* Calculate StoredKey, and encode it in hex */
+ /* Calculate StoredKey, and encode it in base64 */
+ encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
- (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, storedkey_hex);
- storedkey_hex[SCRAM_KEY_LEN * 2] = '\0';
+ encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
+ encoded_storedkey);
+ encoded_storedkey[encoded_len] = '\0';
/* And same for ServerKey */
+ encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
SCRAM_SERVER_KEY_NAME, keybuf);
- (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, serverkey_hex);
- serverkey_hex[SCRAM_KEY_LEN * 2] = '\0';
+ encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
+ encoded_serverkey);
+ encoded_serverkey[encoded_len] = '\0';
- return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations, storedkey_hex, serverkey_hex);
+ return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations,
+ encoded_storedkey, encoded_serverkey);
}
@@ -425,17 +430,16 @@ parse_scram_verifier(const char *verifier, char **salt, int *iterations,
/* storedkey */
if ((p = strtok(NULL, ":")) == NULL)
goto invalid_verifier;
- if (strlen(p) != SCRAM_KEY_LEN * 2)
+ if (strlen(p) != pg_b64_enc_len(SCRAM_KEY_LEN) - 1)
goto invalid_verifier;
-
- hex_decode(p, SCRAM_KEY_LEN * 2, (char *) stored_key);
+ pg_b64_decode(p, pg_b64_enc_len(SCRAM_KEY_LEN), (char *) stored_key);
/* serverkey */
if ((p = strtok(NULL, ":")) == NULL)
goto invalid_verifier;
- if (strlen(p) != SCRAM_KEY_LEN * 2)
+ if (strlen(p) != pg_b64_enc_len(SCRAM_KEY_LEN) - 1)
goto invalid_verifier;
- hex_decode(p, SCRAM_KEY_LEN * 2, (char *) server_key);
+ pg_b64_decode(p, pg_b64_enc_len(SCRAM_KEY_LEN), (char *) server_key);
pfree(v);
return true;
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index c503e43abe..0cdb6141e2 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -23,11 +23,11 @@ CREATE ROLE regress_passwd5 PASSWORD NULL;
-- check list of created entries
--
-- The scram verifier will look something like:
--- scram-sha-256:E4HxLGtnRzsYwg==:4096:5ebc825510cb7862efd87dfa638d8337179e6913a724441dc9e888a856fbc10c:e966b1c72fad89d69aaebb156eae04edc9581286f92207c044711e79cd461bee
+-- scram-sha-256:E4HxLGtnRzsYwg==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo=
--
-- Since the salt is random, the exact value stored will be different on every test
-- run. Use a regular expression to mask the changing parts.
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
@@ -59,11 +59,11 @@ ALTER ROLE regress_passwd1 UNENCRYPTED PASSWORD 'foo'; -- unencrypted
ALTER ROLE regress_passwd2 UNENCRYPTED PASSWORD 'md5dfa155cadd5f4ad57860162f3fab9cdb'; -- encrypted with MD5
SET password_encryption = 'md5';
ALTER ROLE regress_passwd3 ENCRYPTED PASSWORD 'foo'; -- encrypted with MD5
-ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:3ded2376f7aafa93b1bdbd71bcc18b7d6ee50ed018029cc583d152ef3fc7d430:a6dd36dfc94c181956a6ae95f05e01b1864f0a22a2657d1de4ba84d2a24dc438'; -- client-supplied SCRAM verifier, use as it is
+ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; -- client-supplied SCRAM verifier, use as it is
SET password_encryption = 'scram';
ALTER ROLE regress_passwd5 ENCRYPTED PASSWORD 'foo'; -- create SCRAM verifier
CREATE ROLE regress_passwd6 ENCRYPTED PASSWORD 'md53725413363ab045e20521bf36b8d8d7f'; -- encrypted with MD5, use as it is
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql
index f4b3a9ac3a..df1ff989cc 100644
--- a/src/test/regress/sql/password.sql
+++ b/src/test/regress/sql/password.sql
@@ -24,11 +24,11 @@ CREATE ROLE regress_passwd5 PASSWORD NULL;
-- check list of created entries
--
-- The scram verifier will look something like:
--- scram-sha-256:E4HxLGtnRzsYwg==:4096:5ebc825510cb7862efd87dfa638d8337179e6913a724441dc9e888a856fbc10c:e966b1c72fad89d69aaebb156eae04edc9581286f92207c044711e79cd461bee
+-- scram-sha-256:E4HxLGtnRzsYwg==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo=
--
-- Since the salt is random, the exact value stored will be different on every test
-- run. Use a regular expression to mask the changing parts.
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
@@ -48,13 +48,13 @@ ALTER ROLE regress_passwd2 UNENCRYPTED PASSWORD 'md5dfa155cadd5f4ad57860162f3fab
SET password_encryption = 'md5';
ALTER ROLE regress_passwd3 ENCRYPTED PASSWORD 'foo'; -- encrypted with MD5
-ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:3ded2376f7aafa93b1bdbd71bcc18b7d6ee50ed018029cc583d152ef3fc7d430:a6dd36dfc94c181956a6ae95f05e01b1864f0a22a2657d1de4ba84d2a24dc438'; -- client-supplied SCRAM verifier, use as it is
+ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; -- client-supplied SCRAM verifier, use as it is
SET password_encryption = 'scram';
ALTER ROLE regress_passwd5 ENCRYPTED PASSWORD 'foo'; -- create SCRAM verifier
CREATE ROLE regress_passwd6 ENCRYPTED PASSWORD 'md53725413363ab045e20521bf36b8d8d7f'; -- encrypted with MD5, use as it is
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
--
2.12.0
0002-Refactor-frontend-side-random-number-generation.patchapplication/octet-stream; name=0002-Refactor-frontend-side-random-number-generation.patchDownload
From 539f4ea99bd7040045b83d58fe279a5db7dd213d Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 15 Mar 2017 22:06:12 +0900
Subject: [PATCH 2/4] Refactor frontend-side random number generation
pg_frontend_random() is moved into its own file in src/common/ to
give other portions of the code the ability to generate random numbers.
This will be used for the SCRAM verifier generation from clients.
---
src/common/Makefile | 3 +-
src/common/frontend_random.c | 86 ++++++++++++++++++++++++++++++++++++
src/include/common/frontend_random.h | 17 +++++++
src/interfaces/libpq/.gitignore | 1 +
src/interfaces/libpq/Makefile | 4 +-
src/interfaces/libpq/fe-auth-scram.c | 59 +------------------------
src/tools/msvc/Mkvcbuild.pm | 2 +-
7 files changed, 110 insertions(+), 62 deletions(-)
create mode 100644 src/common/frontend_random.c
create mode 100644 src/include/common/frontend_random.h
diff --git a/src/common/Makefile b/src/common/Makefile
index 971ddd5ea7..b516ec43f1 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -50,7 +50,8 @@ else
OBJS_COMMON += sha2.o
endif
-OBJS_FRONTEND = $(OBJS_COMMON) fe_memutils.o file_utils.o restricted_token.o
+OBJS_FRONTEND = $(OBJS_COMMON) fe_memutils.o file_utils.o frontend_random.c \
+ restricted_token.o
OBJS_SRV = $(OBJS_COMMON:%.o=%_srv.o)
diff --git a/src/common/frontend_random.c b/src/common/frontend_random.c
new file mode 100644
index 0000000000..7ecc7d5fdf
--- /dev/null
+++ b/src/common/frontend_random.c
@@ -0,0 +1,86 @@
+/*-------------------------------------------------------------------------
+ *
+ * frontend_random.c
+ * Frontend random number generation routine.
+ *
+ * pg_frontend_random() function fills a buffer with random bytes. Normally,
+ * it is just a thin wrapper around pg_strong_random(), but when compiled
+ * with --disable-strong-random, there is a built-in implementation.
+ *
+ * The built-in implementation uses the standard erand48 algorithm, with
+ * a seed calculated using the process ID and a timestamp.
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/common/frontend_random.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#error "This file is not expected to be compiled for backend code"
+#endif
+
+#include "postgres_fe.h"
+#include "common/frontend_random.h"
+
+/* These are needed for getpid(), in the fallback implementation */
+#ifndef HAVE_STRONG_RANDOM
+#include <sys/types.h>
+#include <unistd.h>
+#endif
+
+/*
+ * Random number generator.
+ */
+bool
+pg_frontend_random(char *dst, int len)
+{
+#ifdef HAVE_STRONG_RANDOM
+ return pg_strong_random(dst, len);
+#else
+ int i;
+ char *end = dst + len;
+
+ static unsigned short seed[3];
+ static int mypid = 0;
+
+ pglock_thread();
+
+ if (mypid != getpid())
+ {
+ struct timeval now;
+
+ gettimeofday(&now, NULL);
+
+ seed[0] = now.tv_sec ^ getpid();
+ seed[1] = (unsigned short) (now.tv_usec);
+ seed[2] = (unsigned short) (now.tv_usec >> 16);
+ }
+
+ for (i = 0; dst < end; i++)
+ {
+ uint32 r;
+ int j;
+
+ /*
+ * pg_jrand48 returns a 32-bit integer. Fill the next 4 bytes from
+ * it.
+ */
+ r = (uint32) pg_jrand48(seed);
+
+ for (j = 0; j < 4 && dst < end; j++)
+ {
+ *(dst++) = (char) (r & 0xFF);
+ r >>= 8;
+ }
+ }
+
+ pgunlock_thread();
+
+ return true;
+#endif
+}
diff --git a/src/include/common/frontend_random.h b/src/include/common/frontend_random.h
new file mode 100644
index 0000000000..600c55ace9
--- /dev/null
+++ b/src/include/common/frontend_random.h
@@ -0,0 +1,17 @@
+/*-------------------------------------------------------------------------
+ *
+ * frontend_random.h
+ * Declarations for frontend random number generation
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ *
+ * src/include/common/frontend_random.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef FRONTEND_RANDOM_H
+#define FRONTEND_RANDOM_H
+
+extern bool pg_frontend_random(char *dst, int len);
+
+#endif /* FRONTEND_RANDOM_H */
diff --git a/src/interfaces/libpq/.gitignore b/src/interfaces/libpq/.gitignore
index 2224ada731..f84bc35706 100644
--- a/src/interfaces/libpq/.gitignore
+++ b/src/interfaces/libpq/.gitignore
@@ -2,6 +2,7 @@
/base64.c
/chklocale.c
/crypt.c
+/frontend_random.c
/getaddrinfo.c
/getpeereid.c
/inet_aton.c
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 792232db49..757a702a37 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -49,7 +49,7 @@ endif
# src/backend/utils/mb
OBJS += encnames.o wchar.o
# src/common
-OBJS += base64.o ip.o md5.o scram-common.o
+OBJS += base64.o frontend_random.o ip.o md5.o scram-common.o
ifeq ($(with_openssl),yes)
OBJS += fe-secure-openssl.o sha2_openssl.o
@@ -111,7 +111,7 @@ ip.c md5.c: % : $(top_srcdir)/src/common/%
encnames.c wchar.c: % : $(backend_src)/utils/mb/%
rm -f $@ && $(LN_S) $< .
-base64.c scram-common.c sha2.c sha2_openssl.c: % : $(top_srcdir)/src/common/%
+base64.c frontend_random.c scram-common.c sha2.c sha2_openssl.c: % : $(top_srcdir)/src/common/%
rm -f $@ && $(LN_S) $< .
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index a7bb30a141..6390ccf30d 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -15,14 +15,10 @@
#include "postgres_fe.h"
#include "common/base64.h"
+#include "common/frontend_random.h"
#include "common/scram-common.h"
#include "fe-auth.h"
-/* These are needed for getpid(), in the fallback implementation */
-#ifndef HAVE_STRONG_RANDOM
-#include <sys/types.h>
-#include <unistd.h>
-#endif
/*
* Status of exchange messages used for SCRAM authentication via the
@@ -73,7 +69,6 @@ static bool verify_server_proof(fe_scram_state *state);
static void calculate_client_proof(fe_scram_state *state,
const char *client_final_message_without_proof,
uint8 *result);
-static bool pg_frontend_random(char *dst, int len);
/*
* Initialize SCRAM exchange status.
@@ -586,55 +581,3 @@ verify_server_proof(fe_scram_state *state)
return true;
}
-
-/*
- * Random number generator.
- */
-static bool
-pg_frontend_random(char *dst, int len)
-{
-#ifdef HAVE_STRONG_RANDOM
- return pg_strong_random(dst, len);
-#else
- int i;
- char *end = dst + len;
-
- static unsigned short seed[3];
- static int mypid = 0;
-
- pglock_thread();
-
- if (mypid != getpid())
- {
- struct timeval now;
-
- gettimeofday(&now, NULL);
-
- seed[0] = now.tv_sec ^ getpid();
- seed[1] = (unsigned short) (now.tv_usec);
- seed[2] = (unsigned short) (now.tv_usec >> 16);
- }
-
- for (i = 0; dst < end; i++)
- {
- uint32 r;
- int j;
-
- /*
- * pg_jrand48 returns a 32-bit integer. Fill the next 4 bytes from
- * it.
- */
- r = (uint32) pg_jrand48(seed);
-
- for (j = 0; j < 4 && dst < end; j++)
- {
- *(dst++) = (char) (r & 0xFF);
- r >>= 8;
- }
- }
-
- pgunlock_thread();
-
- return true;
-#endif
-}
diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm
index 12f73f344c..615c769483 100644
--- a/src/tools/msvc/Mkvcbuild.pm
+++ b/src/tools/msvc/Mkvcbuild.pm
@@ -125,7 +125,7 @@ sub mkvcbuild
our @pgcommonfrontendfiles = (
@pgcommonallfiles, qw(fe_memutils.c file_utils.c
- restricted_token.c));
+ frontend_random.c restricted_token.c));
our @pgcommonbkndfiles = @pgcommonallfiles;
--
2.12.0
0003-Move-routine-to-build-SCRAM-verifier-into-src-common.patchapplication/octet-stream; name=0003-Move-routine-to-build-SCRAM-verifier-into-src-common.patchDownload
From 8e1a53287a75e0000a4823b6aac569f08dcf1599 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 15 Mar 2017 23:45:50 +0900
Subject: [PATCH 3/4] Move routine to build SCRAM verifier into src/common/
This is aimed at being used by libpq to allow frontend-side creation
of SCRAM verifiers.
---
src/backend/libpq/auth-scram.c | 57 ++----------------------------
src/backend/libpq/crypt.c | 7 +++-
src/common/scram-common.c | 74 +++++++++++++++++++++++++++++++++++++++
src/include/common/scram-common.h | 20 +++++++++++
src/include/libpq/scram.h | 5 +--
5 files changed, 103 insertions(+), 60 deletions(-)
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 29e2965883..4ed0e968d7 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -171,14 +171,14 @@ pg_be_scram_init(const char *username, const char *shadow_pass, bool doomed)
}
else if (password_type == PASSWORD_TYPE_PLAINTEXT)
{
- char *verifier;
+ char *verifier = palloc(SCRAM_VERIFIER_LEN + 1);
/*
* The password provided is in plain format, in which case a fresh
* SCRAM verifier can be generated and used for the rest of the
* processing.
*/
- verifier = scram_build_verifier(username, shadow_pass, 0);
+ (void) scram_build_verifier(username, shadow_pass, 0, verifier);
(void) parse_scram_verifier(verifier, &state->salt, &state->iterations,
state->StoredKey, state->ServerKey);
@@ -316,59 +316,6 @@ pg_be_scram_exchange(void *opaq, char *input, int inputlen,
return result;
}
-/*
- * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
- *
- * If iterations is 0, default number of iterations is used. The result is
- * palloc'd, so caller is responsible for freeing it.
- */
-char *
-scram_build_verifier(const char *username, const char *password,
- int iterations)
-{
- uint8 keybuf[SCRAM_KEY_LEN + 1];
- char *encoded_storedkey;
- char *encoded_serverkey;
- char salt[SCRAM_SALT_LEN];
- char *encoded_salt;
- int encoded_len;
-
- if (iterations <= 0)
- iterations = SCRAM_ITERATIONS_DEFAULT;
-
- if (!pg_backend_random(salt, SCRAM_SALT_LEN))
- {
- ereport(LOG,
- (errcode(ERRCODE_INTERNAL_ERROR),
- errmsg("could not generate random salt")));
- return NULL;
- }
-
- encoded_salt = palloc(pg_b64_enc_len(SCRAM_SALT_LEN) + 1);
- encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt);
- encoded_salt[encoded_len] = '\0';
-
- /* Calculate StoredKey, and encode it in base64 */
- encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
- scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
- iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
- scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
- encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
- encoded_storedkey);
- encoded_storedkey[encoded_len] = '\0';
-
- /* And same for ServerKey */
- encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
- scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
- SCRAM_SERVER_KEY_NAME, keybuf);
- encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
- encoded_serverkey);
- encoded_serverkey[encoded_len] = '\0';
-
- return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations,
- encoded_storedkey, encoded_serverkey);
-}
-
/*
* Check if given verifier can be used for SCRAM authentication.
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 9f0ae15b00..9a448fad11 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,6 +20,7 @@
#include "catalog/pg_authid.h"
#include "common/md5.h"
+#include "common/scram-common.h"
#include "libpq/crypt.h"
#include "libpq/scram.h"
#include "miscadmin.h"
@@ -168,7 +169,11 @@ encrypt_password(PasswordType target_type, const char *role,
switch (guessed_type)
{
case PASSWORD_TYPE_PLAINTEXT:
- return scram_build_verifier(role, password, 0);
+ encrypted_password = palloc(SCRAM_VERIFIER_LEN + 1);
+ if (!scram_build_verifier(role, password, 0,
+ encrypted_password))
+ elog(ERROR, "password encryption failed");
+ return encrypted_password;
case PASSWORD_TYPE_MD5:
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index e44f38f652..00e45a800a 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -24,6 +24,11 @@
#include <arpa/inet.h>
#include "common/scram-common.h"
+#ifndef FRONTEND
+#include "utils/backend_random.h"
+#else
+#include "common/frontend_random.h"
+#endif
#define HMAC_IPAD 0x36
#define HMAC_OPAD 0x5C
@@ -184,3 +189,72 @@ scram_ClientOrServerKey(const char *password,
scram_HMAC_update(&ctx, keystr, strlen(keystr));
scram_HMAC_final(result, &ctx);
}
+
+/*
+ * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
+ *
+ * If iterations is 0, default number of iterations is used. The result is
+ * stored in "verifier" that caller is responsible to allocate a buffer of
+ * size SCRAM_VERIFIER_LEN. Returns true if the verifier has been generated,
+ * false otherwise. It is important for this routine to do no memory
+ * allocations.
+ */
+bool
+scram_build_verifier(const char *username, const char *password,
+ int iterations, char *verifier)
+{
+ uint8 keybuf[SCRAM_KEY_LEN + 1];
+ char salt[SCRAM_SALT_LEN];
+ char intbuf[SCRAM_ITERATION_LEN];
+ char *p;
+
+ if (iterations <= 0)
+ iterations = SCRAM_ITERATIONS_DEFAULT;
+
+#ifdef FRONTEND
+ if (!pg_frontend_random(salt, SCRAM_SALT_LEN))
+ return false;
+#else
+ if (!pg_backend_random(salt, SCRAM_SALT_LEN))
+ {
+ ereport(LOG,
+ (errcode(ERRCODE_INTERNAL_ERROR),
+ errmsg("could not generate random salt")));
+ return false;
+ }
+#endif
+
+ /* Fill in the data of the verifier */
+ p = verifier;
+ memcpy(p, SCRAM_VERIFIER_PREFIX, strlen(SCRAM_VERIFIER_PREFIX));
+ p += strlen(SCRAM_VERIFIER_PREFIX);
+ *p++ = ':';
+
+ /* salt */
+ (void) pg_b64_encode(salt, SCRAM_SALT_LEN, p);
+ p += pg_b64_enc_len(SCRAM_SALT_LEN);
+ *p++ = ':';
+
+ /* iterations */
+ sprintf(intbuf, "%d", iterations);
+ memcpy(p, intbuf, strlen(intbuf));
+ p += strlen(intbuf);
+ *p++ = ':';
+
+ /* Calculate StoredKey, and encode it in base64 */
+ scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
+ iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
+ scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
+ (void) pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, p);
+ p += pg_b64_enc_len(SCRAM_KEY_LEN) - 1;
+ *p++ = ':';
+
+ /* And same for ServerKey */
+ scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
+ SCRAM_SERVER_KEY_NAME, keybuf);
+ (void) pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, p);
+ p += pg_b64_enc_len(SCRAM_KEY_LEN) - 1;
+ *p++ = '\0';
+
+ return true;
+}
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index 7c98cc74d6..3fdfca44d6 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -13,6 +13,7 @@
#ifndef SCRAM_COMMON_H
#define SCRAM_COMMON_H
+#include "common/base64.h"
#include "common/sha2.h"
/* Length of SCRAM keys (client and server) */
@@ -41,6 +42,23 @@
#define SCRAM_SERVER_KEY_NAME "Server Key"
#define SCRAM_CLIENT_KEY_NAME "Client Key"
+#define SCRAM_VERIFIER_PREFIX "scram-sha-256"
+
+/*
+ * Length of a SCRAM verifier, which is made of the following five fields
+ * separated by a colon:
+ * - prefix "scram-sha-256", made of 13 characters.
+ * - 4 colon separators.
+ * - 32-bit number of interations, up to 10 characters.
+ * - base64-encoded salt of length SCRAM_SALT_LEN
+ * - base64-encoded stored key of length SCRAM_KEY_LEN
+ * - base64-encoded server key of length SCRAM_KEY_LEN
+ */
+#define SCRAM_VERIFIER_LEN (strlen("scram-sha-256") + 4 + \
+ SCRAM_ITERATION_LEN + \
+ pg_b64_enc_len(SCRAM_SALT_LEN) + \
+ pg_b64_enc_len(SCRAM_KEY_LEN) * 2)
+
/*
* Context data for HMAC used in SCRAM authentication.
*/
@@ -58,5 +76,7 @@ extern void scram_H(const uint8 *str, int len, uint8 *result);
extern void scram_ClientOrServerKey(const char *password, const char *salt,
int saltlen, int iterations,
const char *keystr, uint8 *result);
+extern bool scram_build_verifier(const char *username, const char *password,
+ int iterations, char *verifier);
#endif /* SCRAM_COMMON_H */
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 78a52db684..3aaa9ef18a 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -26,10 +26,7 @@ extern void *pg_be_scram_init(const char *username, const char *shadow_pass, boo
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
-/* Routines to handle and check SCRAM-SHA-256 verifier */
-extern char *scram_build_verifier(const char *username,
- const char *password,
- int iterations);
+/* Routines to check SCRAM-SHA-256 verifier */
extern bool is_scram_verifier(const char *verifier);
#endif /* PG_SCRAM_H */
--
2.12.0
On 03/15/2017 08:48 AM, Michael Paquier wrote:
I have been hacking my way through this thing, and making
scram_build_verifier is requiring a bit more refactoring than I
thought first:
- stored and server keys are hex-encoded using a backend-only routine.
I think that those should be instead base64-encoded using what has
already been moved in src/common/.
Or possibly make the hex routines available in the front end as well
instead?
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
On Thu, Mar 16, 2017 at 6:46 AM, Joe Conway <mail@joeconway.com> wrote:
On 03/15/2017 08:48 AM, Michael Paquier wrote:
I have been hacking my way through this thing, and making
scram_build_verifier is requiring a bit more refactoring than I
thought first:
- stored and server keys are hex-encoded using a backend-only routine.
I think that those should be instead base64-encoded using what has
already been moved in src/common/.Or possibly make the hex routines available in the front end as well
instead?
Yeah, but keeping in mind that src/common/ should be kept as small as
necessary, the base64 switch made more sense (hex_encode is really
small I know).
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/14/2017 11:14 PM, Tom Lane wrote:
In short, I don't think that argument refutes my position that "md5"
in pg_hba.conf should be understood as allowing SCRAM passwords too.
Yeah, let's do that. Here's a patch.
I had some terminology trouble with the docs. What do you call a user
that has "md5XXXXX" in pgauthid.rolpassword? What about someone with a
SCRAM verifier? I used the terms "those users that have an MD5 hash set
in the system catalog", and "users that have set their password as a
SCRAM verifier", but it feels awkward.
The behavior when a user doesn't exist, or doesn't have a valid
password, is a bit subtle. Previously, with 'md5' authentication, we
would send the client an MD5 challenge, and fail with "invalid password"
error after receiving the response. And with 'scram' authentication, we
would perform a dummy authentication exchange, with a made-up salt. This
is to avoid revealing to an unauthenticated client whether or not the
user existed.
With this patch, the dummy authentication logic for 'md5' is a bit more
complicated. I made it look at the password_encryption GUC, and send the
client a dummy MD5 or SCRAM challenge based on that. The idea is that
most users presumably have a password of that type, so we use that
method for the dummy authentication, to make it look as "normal" as
possible. It's not perfect, if password_encryption is set to 'scram',
and you probe for a user that has an MD5 password set, you can tell that
it's a valid user from the fact that the server sends an MD5 challenge.
In practice, I'm not sure how good this dummy authentication thing
really is anyway. Even on older versions, I'd wager a guess that if you
tried hard enough, you could tell if a user exists or not based on
timing, for example. So I think this is good enough. But it's worth
noting and discussing.
- Heikki
Attachments:
0001-Allow-SCRAM-authentication-when-pg_hba.conf-says-md5.patchtext/plain; charset=UTF-8; name=0001-Allow-SCRAM-authentication-when-pg_hba.conf-says-md5.patchDownload
From 4a6856c1becca8905a5255661b6b64b1aed64ec8 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Thu, 16 Mar 2017 15:45:16 +0200
Subject: [PATCH 1/1] 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. In the
worst case, 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 switched to SCRAM 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.
---
doc/src/sgml/client-auth.sgml | 38 ++++-----
src/backend/libpq/auth-scram.c | 104 ++++++++++++++-----------
src/backend/libpq/auth.c | 173 ++++++++++++++++++++++++++---------------
src/backend/libpq/crypt.c | 44 ++++-------
src/backend/libpq/hba.c | 2 +-
src/include/libpq/crypt.h | 3 +-
src/include/libpq/hba.h | 2 +-
src/include/libpq/scram.h | 2 +-
8 files changed, 207 insertions(+), 161 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index bbd52a5418..db200d4b76 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -412,23 +412,22 @@ hostnossl <replaceable>database</replaceable> <replaceable>user</replaceable>
</varlistentry>
<varlistentry>
- <term><literal>md5</></term>
+ <term><literal>scram</></term>
<listitem>
<para>
- Require the client to supply a double-MD5-hashed password for
- authentication.
- See <xref linkend="auth-password"> for details.
+ Perform SCRAM-SHA-256 authentication to verify the user's
+ password. See <xref linkend="auth-password"> for details.
</para>
</listitem>
</varlistentry>
<varlistentry>
- <term><literal>scram</></term>
+ <term><literal>md5</></term>
<listitem>
<para>
- Perform SCRAM-SHA-256 authentication to verify the user's
- password.
- See <xref linkend="auth-password"> for details.
+ Perform SCRAM-SHA-256 or MD5 authentication to verify the
+ user's password. See <xref linkend="auth-password">
+ for details.
</para>
</listitem>
</varlistentry>
@@ -689,13 +688,12 @@ host postgres all 192.168.12.10/32 scram
# Allow any user from hosts in the example.com domain to connect to
# any database if the user's password is correctly supplied.
#
-# Most users use SCRAM authentication, but some users use older clients
-# that don't support SCRAM authentication, and need to be able to log
-# in using MD5 authentication. Such users are put in the @md5users
-# group, everyone else must use SCRAM.
+# Require SCRAM authentication for most users, but make an exception
+# for user 'mike', who uses an older client that doesn't support SCRAM
+# authentication.
#
# TYPE DATABASE USER ADDRESS METHOD
-host all @md5users .example.com md5
+host all mike .example.com md5
host all all .example.com scram
# In the absence of preceding "host" lines, these two lines will
@@ -949,12 +947,14 @@ omicron bryanh guest1
</para>
<para>
- In <literal>md5</>, the client sends a hash of a random challenge,
- generated by the server, and the password. It prevents password sniffing,
- but is less secure than <literal>scram</>, and provides no protection
- if an attacker manages to steal the password hash from the server.
- <literal>md5</> cannot be used with the <xref
- linkend="guc-db-user-namespace"> feature.
+ <literal>md5</> allows falling back to a less secure challenge-response
+ mechanism for those users that have an MD5 hash set in the system catalog.
+ The fallback mechanism also prevents password sniffing, but provides no
+ protection if an attacker manages to steal the password hash from the
+ server, and it cannot be used with the <xref
+ linkend="guc-db-user-namespace"> feature. For users that have set their
+ password as a SCRAM verifier, <literal>md5</> works the same as
+ <literal>scram</>.
</para>
<para>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 9f78e57aae..c1a229c804 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -130,79 +130,91 @@ static char *scram_MockSalt(const char *username);
* after the beginning of the exchange with verifier data.
*
* 'username' is the provided by the client. 'shadow_pass' is the role's
- * password verifier, from pg_authid.rolpassword. If 'doomed' is true, the
- * authentication must fail, as if an incorrect password was given.
- * 'shadow_pass' may be NULL, when 'doomed' is set.
+ * password verifier, from pg_authid.rolpassword. If 'shadow_pass' is NULL, we
+ * still perform an authentication exchange, but it will fail, as if an
+ * incorrect password was given.
*/
void *
-pg_be_scram_init(const char *username, const char *shadow_pass, bool doomed)
+pg_be_scram_init(const char *username, const char *shadow_pass)
{
scram_state *state;
- int password_type;
+ bool got_verifier;
state = (scram_state *) palloc0(sizeof(scram_state));
state->state = SCRAM_AUTH_INIT;
state->username = username;
/*
- * Perform sanity checks on the provided password after catalog lookup.
- * The authentication is bound to fail if the lookup itself failed or if
- * the password stored is MD5-encrypted. Authentication is possible for
- * users with a valid plain password though.
+ * Parse the stored password verifier.
*/
+ if (shadow_pass)
+ {
+ int password_type = get_password_type(shadow_pass);
- if (shadow_pass == NULL || doomed)
- password_type = -1;
- else
- password_type = get_password_type(shadow_pass);
+ if (password_type == PASSWORD_TYPE_SCRAM)
+ {
+ if (parse_scram_verifier(shadow_pass, &state->salt, &state->iterations,
+ state->StoredKey, state->ServerKey))
+ got_verifier = true;
+ else
+ {
+ /*
+ * The password looked like a SCRAM verifier, but could not be
+ * parsed.
+ */
+ elog(LOG, "invalid SCRAM verifier for user \"%s\"", username);
+ got_verifier = false;
+ }
+ }
+ else if (password_type == PASSWORD_TYPE_PLAINTEXT)
+ {
+ /*
+ * The stored password is in plain format. Generate a fresh SCRAM
+ * verifier from it, and proceed with that.
+ */
+ char *verifier;
- if (password_type == PASSWORD_TYPE_SCRAM)
- {
- if (!parse_scram_verifier(shadow_pass, &state->salt, &state->iterations,
- state->StoredKey, state->ServerKey))
+ verifier = scram_build_verifier(username, shadow_pass, 0);
+
+ (void) parse_scram_verifier(verifier, &state->salt, &state->iterations,
+ state->StoredKey, state->ServerKey);
+ pfree(verifier);
+
+ got_verifier = true;
+ }
+ else
{
/*
- * The password looked like a SCRAM verifier, but could not be
- * parsed.
+ * The user doesn't have SCRAM verifier, nor could we generate
+ * one. (You cannot do SCRAM authentication with an MD5 hash.)
*/
- elog(LOG, "invalid SCRAM verifier for user \"%s\"", username);
- doomed = true;
+ state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."),
+ state->username);
+ got_verifier = false;
}
}
- else if (password_type == PASSWORD_TYPE_PLAINTEXT)
+ else
{
- char *verifier;
-
/*
- * The password provided is in plain format, in which case a fresh
- * SCRAM verifier can be generated and used for the rest of the
- * processing.
+ * The caller requested us to perform a dummy authentication. This is
+ * considered normal, since the caller requested it, so don't set log
+ * detail.
*/
- verifier = scram_build_verifier(username, shadow_pass, 0);
-
- (void) parse_scram_verifier(verifier, &state->salt, &state->iterations,
- state->StoredKey, state->ServerKey);
- pfree(verifier);
+ got_verifier = false;
}
- else
- doomed = true;
- if (doomed)
+ /*
+ * If the user did not have a valid SCRAM verifier, we still go through
+ * the motions with a mock one, and fail as if the client supplied an
+ * incorrect password. This is to avoid revealing information to an
+ * attacker.
+ */
+ if (!got_verifier)
{
- /*
- * We don't have a valid SCRAM verifier, nor could we generate one, or
- * the caller requested us to perform a dummy authentication.
- *
- * The authentication is bound to fail, but to avoid revealing
- * information to the attacker, go through the motions with a fake
- * SCRAM verifier, and fail as if the password was incorrect.
- */
- state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."),
- state->username);
mock_scram_verifier(username, &state->salt, &state->iterations,
state->StoredKey, state->ServerKey);
+ state->doomed = true;
}
- state->doomed = doomed;
return state;
}
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index ebf10bbbae..27ed11a554 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -24,6 +24,7 @@
#include <sys/select.h>
#endif
+#include "commands/user.h"
#include "common/ip.h"
#include "common/md5.h"
#include "libpq/auth.h"
@@ -49,17 +50,15 @@ static char *recv_password_packet(Port *port);
/*----------------------------------------------------------------
- * MD5 authentication
+ * Password-based authentication methods (password, md5, and scram)
*----------------------------------------------------------------
*/
-static int CheckMD5Auth(Port *port, char **logdetail);
+static int CheckPasswordAuth(Port *port, char **logdetail);
+static int CheckPWChallengeAuth(Port *port, char **logdetail);
-/*----------------------------------------------------------------
- * Plaintext password authentication
- *----------------------------------------------------------------
- */
+static int CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail);
+static int CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail);
-static int CheckPasswordAuth(Port *port, char **logdetail);
/*----------------------------------------------------------------
* Ident authentication
@@ -199,12 +198,6 @@ static int pg_SSPI_make_upn(char *accountname,
static int CheckRADIUSAuth(Port *port);
-/*----------------------------------------------------------------
- * SASL authentication
- *----------------------------------------------------------------
- */
-static int CheckSASLAuth(Port *port, char **logdetail);
-
/*
* Maximum accepted size of GSS and SSPI authentication tokens.
*
@@ -290,7 +283,7 @@ auth_failed(Port *port, int status, char *logdetail)
break;
case uaPassword:
case uaMD5:
- case uaSASL:
+ case uaSCRAM:
errstr = gettext_noop("password authentication failed for user \"%s\"");
/* We use it to indicate if a .pgpass password failed. */
errcode_return = ERRCODE_INVALID_PASSWORD;
@@ -551,17 +544,14 @@ ClientAuthentication(Port *port)
break;
case uaMD5:
- status = CheckMD5Auth(port, &logdetail);
+ case uaSCRAM:
+ status = CheckPWChallengeAuth(port, &logdetail);
break;
case uaPassword:
status = CheckPasswordAuth(port, &logdetail);
break;
- case uaSASL:
- status = CheckSASLAuth(port, &logdetail);
- break;
-
case uaPAM:
#ifdef USE_PAM
status = CheckPAMAuth(port, port->user_name, "");
@@ -709,41 +699,30 @@ recv_password_packet(Port *port)
/*----------------------------------------------------------------
- * MD5 authentication
+ * Password-based authentication mechanisms
*----------------------------------------------------------------
*/
+/*
+ * Plaintext password authentication.
+ */
static int
-CheckMD5Auth(Port *port, char **logdetail)
+CheckPasswordAuth(Port *port, char **logdetail)
{
- char md5Salt[4]; /* Password salt */
char *passwd;
- char *shadow_pass;
int result;
+ char *shadow_pass;
- if (Db_user_namespace)
- ereport(FATAL,
- (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
- errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled")));
-
- /* include the salt to use for computing the response */
- if (!pg_backend_random(md5Salt, 4))
- {
- ereport(LOG,
- (errmsg("could not generate random MD5 salt")));
- return STATUS_ERROR;
- }
-
- sendAuthRequest(port, AUTH_REQ_MD5, md5Salt, 4);
+ sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
passwd = recv_password_packet(port);
if (passwd == NULL)
return STATUS_EOF; /* client wouldn't send password */
- result = get_role_password(port->user_name, &shadow_pass, logdetail);
+ shadow_pass = get_role_password(port->user_name, logdetail);
if (result == STATUS_OK)
- result = md5_crypt_verify(port->user_name, shadow_pass, passwd,
- md5Salt, 4, logdetail);
+ result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
+ logdetail);
if (shadow_pass)
pfree(shadow_pass);
@@ -752,42 +731,114 @@ CheckMD5Auth(Port *port, char **logdetail)
return result;
}
-/*----------------------------------------------------------------
- * Plaintext password authentication
- *----------------------------------------------------------------
+/*
+ * MD5 and SCRAM authentication.
*/
+static int
+CheckPWChallengeAuth(Port *port, char **logdetail)
+{
+ int auth_result;
+ char *shadow_pass;
+ PasswordType pwtype;
+
+ Assert(port->hba->auth_method == uaSCRAM ||
+ port->hba->auth_method == uaMD5);
+
+ /* First look up the user's password. */
+ shadow_pass = get_role_password(port->user_name, logdetail);
+
+ /*
+ * If the user does not exist, or has no password, we still go through the
+ * motions of authentication, to avoid revealing to the client that the
+ * user didn't exist. If 'md5' is allowed, we choose whether to use 'md5'
+ * or 'scram' authentication based on current password_encryption setting.
+ * The idea is that most genuine users probably have a password of that
+ * type, if we pretend that this user had a password of that type, too, it
+ * "blends in" best.
+ *
+ * If the user had a password, but it was expired, we'll use the details
+ * of the expired password for the authentication, but report it as
+ * failure to the client even if correct password was given.
+ */
+ if (!shadow_pass)
+ pwtype = Password_encryption;
+ else
+ pwtype = get_password_type(shadow_pass);
+
+ /*
+ * If 'md5' authentication is allowed, decide whether to perform 'md5' or
+ * 'scram' authentication based on the type of password the user has. If
+ * it's an MD5 hash, we must do MD5 authentication, and if it's a SCRAM
+ * verifier, we must do SCRAM authentication. If it's stored in
+ * plaintext, we could do either one, so we opt for the more secure
+ * mechanism, SCRAM.
+ *
+ * If MD5 authentication is not allowed, always use SCRAM. If the user
+ * had an MD5 password, CheckSCRAMAuth() will fail.
+ */
+ if (port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5)
+ {
+ auth_result = CheckMD5Auth(port, shadow_pass, logdetail);
+ }
+ else
+ {
+ auth_result = CheckSCRAMAuth(port, shadow_pass, logdetail);
+ }
+
+ if (shadow_pass)
+ pfree(shadow_pass);
+
+ /*
+ * If get_role_password() returned error, return error, even if the
+ * authentication succeeded.
+ */
+ if (!shadow_pass)
+ {
+ Assert(auth_result != STATUS_OK);
+ return STATUS_ERROR;
+ }
+ return auth_result;
+}
static int
-CheckPasswordAuth(Port *port, char **logdetail)
+CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail)
{
+ char md5Salt[4]; /* Password salt */
char *passwd;
int result;
- char *shadow_pass;
- sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
+ if (Db_user_namespace)
+ ereport(FATAL,
+ (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
+ errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled")));
+
+ /* include the salt to use for computing the response */
+ if (!pg_backend_random(md5Salt, 4))
+ {
+ ereport(LOG,
+ (errmsg("could not generate random MD5 salt")));
+ return STATUS_ERROR;
+ }
+
+ sendAuthRequest(port, AUTH_REQ_MD5, md5Salt, 4);
passwd = recv_password_packet(port);
if (passwd == NULL)
return STATUS_EOF; /* client wouldn't send password */
- result = get_role_password(port->user_name, &shadow_pass, logdetail);
- if (result == STATUS_OK)
- result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
- logdetail);
-
if (shadow_pass)
- pfree(shadow_pass);
+ result = md5_crypt_verify(port->user_name, shadow_pass, passwd,
+ md5Salt, 4, logdetail);
+ else
+ result = STATUS_ERROR;
+
pfree(passwd);
return result;
}
-/*----------------------------------------------------------------
- * SASL authentication system
- *----------------------------------------------------------------
- */
static int
-CheckSASLAuth(Port *port, char **logdetail)
+CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
{
int mtype;
StringInfoData buf;
@@ -795,8 +846,6 @@ CheckSASLAuth(Port *port, char **logdetail)
char *output = NULL;
int outputlen = 0;
int result;
- char *shadow_pass;
- bool doomed = false;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -826,11 +875,9 @@ CheckSASLAuth(Port *port, char **logdetail)
* This is because we don't want to reveal to an attacker what usernames
* are valid, nor which users have a valid password.
*/
- if (get_role_password(port->user_name, &shadow_pass, logdetail) != STATUS_OK)
- doomed = true;
/* Initialize the status tracker for message exchanges */
- scram_opaq = pg_be_scram_init(port->user_name, shadow_pass, doomed);
+ scram_opaq = pg_be_scram_init(port->user_name, shadow_pass);
/*
* Loop through SASL message exchange. This exchange can consist of
@@ -874,7 +921,7 @@ CheckSASLAuth(Port *port, char **logdetail)
*/
result = pg_be_scram_exchange(scram_opaq, buf.data, buf.len,
&output, &outputlen,
- doomed ? NULL : logdetail);
+ logdetail);
/* input buffer no longer used */
pfree(buf.data);
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 9f0ae15b00..1c5a3fa5f3 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -31,25 +31,18 @@
/*
* Fetch stored password for a user, for authentication.
*
- * Returns STATUS_OK on success. On error, returns STATUS_ERROR, and stores
- * a palloc'd string describing the reason, for the postmaster log, in
- * *logdetail. The error reason should *not* be sent to the client, to avoid
- * giving away user information!
- *
- * If the password is expired, it is still returned in *shadow_pass, but the
- * return code is STATUS_ERROR. On other errors, *shadow_pass is set to
- * NULL.
+ * On error, returns NULL, and stores a palloc'd string describing the reason,
+ * for the postmaster log, in *logdetail. The error reason should *not* be
+ * sent to the client, to avoid giving away user information!
*/
-int
-get_role_password(const char *role, char **shadow_pass, char **logdetail)
+char *
+get_role_password(const char *role, char **logdetail)
{
- int retval = STATUS_ERROR;
TimestampTz vuntil = 0;
HeapTuple roleTup;
Datum datum;
bool isnull;
-
- *shadow_pass = NULL;
+ char *shadow_pass;
/* Get role info from pg_authid */
roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -57,7 +50,7 @@ get_role_password(const char *role, char **shadow_pass, char **logdetail)
{
*logdetail = psprintf(_("Role \"%s\" does not exist."),
role);
- return STATUS_ERROR; /* no such user */
+ return NULL; /* no such user */
}
datum = SysCacheGetAttr(AUTHNAME, roleTup,
@@ -67,9 +60,9 @@ get_role_password(const char *role, char **shadow_pass, char **logdetail)
ReleaseSysCache(roleTup);
*logdetail = psprintf(_("User \"%s\" has no password assigned."),
role);
- return STATUS_ERROR; /* user has no password */
+ return NULL; /* user has no password */
}
- *shadow_pass = TextDatumGetCString(datum);
+ shadow_pass = TextDatumGetCString(datum);
datum = SysCacheGetAttr(AUTHNAME, roleTup,
Anum_pg_authid_rolvaliduntil, &isnull);
@@ -78,30 +71,25 @@ get_role_password(const char *role, char **shadow_pass, char **logdetail)
ReleaseSysCache(roleTup);
- if (**shadow_pass == '\0')
+ if (*shadow_pass == '\0')
{
*logdetail = psprintf(_("User \"%s\" has an empty password."),
role);
- pfree(*shadow_pass);
- *shadow_pass = NULL;
- return STATUS_ERROR; /* empty password */
+ pfree(shadow_pass);
+ return NULL; /* empty password */
}
/*
- * Password OK, now check to be sure we are not past rolvaliduntil
+ * Password OK, but check to be sure we are not past rolvaliduntil
*/
- if (isnull)
- retval = STATUS_OK;
- else if (vuntil < GetCurrentTimestamp())
+ if (!isnull && vuntil < GetCurrentTimestamp())
{
*logdetail = psprintf(_("User \"%s\" has an expired password."),
role);
- retval = STATUS_ERROR;
+ return NULL;
}
- else
- retval = STATUS_OK;
- return retval;
+ return shadow_pass;
}
/*
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 3817d249c4..e2c1a86901 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1325,7 +1325,7 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
parsedline->auth_method = uaMD5;
}
else if (strcmp(token->string, "scram") == 0)
- parsedline->auth_method = uaSASL;
+ parsedline->auth_method = uaSCRAM;
else if (strcmp(token->string, "pam") == 0)
#ifdef USE_PAM
parsedline->auth_method = uaPAM;
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index 0502d6a0e5..3b5da69b08 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -32,8 +32,7 @@ extern PasswordType get_password_type(const char *shadow_pass);
extern char *encrypt_password(PasswordType target_type, const char *role,
const char *password);
-extern int get_role_password(const char *role, char **shadow_pass,
- char **logdetail);
+extern char *get_role_password(const char *role, char **logdetail);
extern int md5_crypt_verify(const char *role, const char *shadow_pass,
const char *client_pass, const char *md5_salt,
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f55edb16a..4925e9d609 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -30,7 +30,7 @@ typedef enum UserAuth
uaIdent,
uaPassword,
uaMD5,
- uaSASL,
+ uaSCRAM,
uaGSS,
uaSSPI,
uaPAM,
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 78a52db684..7cc47a757e 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -22,7 +22,7 @@
#define SASL_EXCHANGE_FAILURE 2
/* Routines dedicated to authentication */
-extern void *pg_be_scram_init(const char *username, const char *shadow_pass, bool doomed);
+extern void *pg_be_scram_init(const char *username, const char *shadow_pass);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
--
2.11.0
On 03/16/2017 06:52 AM, Heikki Linnakangas wrote:
On 03/14/2017 11:14 PM, Tom Lane wrote:
In short, I don't think that argument refutes my position that "md5"
in pg_hba.conf should be understood as allowing SCRAM passwords too.Yeah, let's do that. Here's a patch.
I had some terminology trouble with the docs. What do you call a user
that has "md5XXXXX" in pgauthid.rolpassword? What about someone with a
SCRAM verifier? I used the terms "those users that have an MD5 hash set
in the system catalog", and "users that have set their password as a
SCRAM verifier", but it feels awkward.
maybe something like:
"those users with an MD5 hashed password"
"those users with a SCRAM verifier hash"
The behavior when a user doesn't exist, or doesn't have a valid
password, is a bit subtle. Previously, with 'md5' authentication, we
would send the client an MD5 challenge, and fail with "invalid password"
error after receiving the response. And with 'scram' authentication, we
would perform a dummy authentication exchange, with a made-up salt. This
is to avoid revealing to an unauthenticated client whether or not the
user existed.With this patch, the dummy authentication logic for 'md5' is a bit more
complicated. I made it look at the password_encryption GUC, and send the
client a dummy MD5 or SCRAM challenge based on that. The idea is that
most users presumably have a password of that type, so we use that
method for the dummy authentication, to make it look as "normal" as
possible. It's not perfect, if password_encryption is set to 'scram',
and you probe for a user that has an MD5 password set, you can tell that
it's a valid user from the fact that the server sends an MD5 challenge.
Presumably if you are unauthenticated you don't have any way to know
what password_encryption is set to, so this seems pretty reasonable.
Joe
--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development
On Thu, Mar 16, 2017 at 10:52 PM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 03/14/2017 11:14 PM, Tom Lane wrote:
In short, I don't think that argument refutes my position that "md5"
in pg_hba.conf should be understood as allowing SCRAM passwords too.Yeah, let's do that. Here's a patch.
At least this has the merit of making \password simpler from psql
without a kind of --method option: if the backend is 9.6 or older,
just generate a MD5-hash, and SCRAM-hash for newer versions.
PQencryptPassword still needs to be extended so as it accepts a hash
method though.
I had some terminology trouble with the docs. What do you call a user that
has "md5XXXXX" in pgauthid.rolpassword? What about someone with a SCRAM
verifier? I used the terms "those users that have an MD5 hash set in the
system catalog", and "users that have set their password as a SCRAM
verifier", but it feels awkward.
MD5-hashed values are verifiers as well, the use of the term
"password" looks weird to me.
The behavior when a user doesn't exist, or doesn't have a valid password, is
a bit subtle. Previously, with 'md5' authentication, we would send the
client an MD5 challenge, and fail with "invalid password" error after
receiving the response. And with 'scram' authentication, we would perform a
dummy authentication exchange, with a made-up salt. This is to avoid
revealing to an unauthenticated client whether or not the user existed.With this patch, the dummy authentication logic for 'md5' is a bit more
complicated. I made it look at the password_encryption GUC, and send the
client a dummy MD5 or SCRAM challenge based on that. The idea is that most
users presumably have a password of that type, so we use that method for the
dummy authentication, to make it look as "normal" as possible. It's not
perfect, if password_encryption is set to 'scram', and you probe for a user
that has an MD5 password set, you can tell that it's a valid user from the
fact that the server sends an MD5 challenge.
No objections to that. If password_encryption is set off or plain, it
is definitely better to switch to scram as this patch does.
In practice, I'm not sure how good this dummy authentication thing really is
anyway. Even on older versions, I'd wager a guess that if you tried hard
enough, you could tell if a user exists or not based on timing, for example.
So I think this is good enough. But it's worth noting and discussing.
Regression tests are proving to be useful here (it would be nice to
get those committed first!). I am noticing that this patch breaks
connection for users with cleartext or md5-hashed verifier when
"password" is used in pg_hba.conf. The case where a user has a
scram-hashed verifier when "md5" is used in the matching hba entry
works though. The missing piece is visibly in CheckPWChallengeAuth(),
which should also be used with uaPassword.
-# Most users use SCRAM authentication, but some users use older clients
-# that don't support SCRAM authentication, and need to be able to log
-# in using MD5 authentication. Such users are put in the @md5users
-# group, everyone else must use SCRAM.
+# Require SCRAM authentication for most users, but make an exception
+# for user 'mike', who uses an older client that doesn't support SCRAM
+# authentication.
#
# TYPE DATABASE USER ADDRESS METHOD
-host all @md5users .example.com md5
+host all mike .example.com md5
Why not still using @md5users?
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Mar 16, 2017 at 11:38 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:
On Thu, Mar 16, 2017 at 10:52 PM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 03/14/2017 11:14 PM, Tom Lane wrote:
In short, I don't think that argument refutes my position that "md5"
in pg_hba.conf should be understood as allowing SCRAM passwords too.Yeah, let's do that. Here's a patch.
At least this has the merit of making \password simpler from psql
without a kind of --method option: if the backend is 9.6 or older,
just generate a MD5-hash, and SCRAM-hash for newer versions.
PQencryptPassword still needs to be extended so as it accepts a hash
method though.
What if the user doesn't want to switch to SCRAM because they also use
some connector that hasn't been updated to support it?
I bet there will be a lot of people in that situation.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/17/2017 02:01 PM, Robert Haas wrote:
On Thu, Mar 16, 2017 at 11:38 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:At least this has the merit of making \password simpler from psql
without a kind of --method option: if the backend is 9.6 or older,
just generate a MD5-hash, and SCRAM-hash for newer versions.
PQencryptPassword still needs to be extended so as it accepts a hash
method though.What if the user doesn't want to switch to SCRAM because they also use
some connector that hasn't been updated to support it?I bet there will be a lot of people in that situation.
You could use the less secure server-side ALTER USER to set the password
in that case. Or use an older client. But I agree, it's a bit weird if
we don't allow the user to generate an MD5 hash, if he insists. I think
we still need a 'method' option to \password.
It would make sense to have \password obey password_encryption GUC. Then
\password and ALTER USER would do the same thing, which would be less
surprising. Although it's also a bit weird for a GUC to affect
client-side behavior, so perhaps better to just document that \password
will create a SCRAM verifier, unless you explicitly tell it to create an
MD5 hash, and add a 'method' parameter to it.
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Mar 17, 2017 at 8:32 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
It would make sense to have \password obey password_encryption GUC. Then
\password and ALTER USER would do the same thing, which would be less
surprising. Although it's also a bit weird for a GUC to affect client-side
behavior, so perhaps better to just document that \password will create a
SCRAM verifier, unless you explicitly tell it to create an MD5 hash, and add
a 'method' parameter to it.
Either of those would be fine with me, but I think we should do one of them.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas <robertmhaas@gmail.com> writes:
On Fri, Mar 17, 2017 at 8:32 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
It would make sense to have \password obey password_encryption GUC. Then
\password and ALTER USER would do the same thing, which would be less
surprising. Although it's also a bit weird for a GUC to affect client-side
behavior, so perhaps better to just document that \password will create a
SCRAM verifier, unless you explicitly tell it to create an MD5 hash, and add
a 'method' parameter to it.
Either of those would be fine with me, but I think we should do one of them.
I vote for the second one; seems much less surprising and action-at-a-
distance-y. And I think the entire point of \password is to *not* do
exactly what a bare ALTER USER would do, but to superimpose a layer of
best practice on it. We certainly want to define use of SCRAM as being
best practice.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/17/2017 05:38 AM, Michael Paquier wrote:
Regression tests are proving to be useful here (it would be nice to
get those committed first!). I am noticing that this patch breaks
connection for users with cleartext or md5-hashed verifier when
"password" is used in pg_hba.conf.
Are you sure? It works for me.
Here's a slightly updated patch that includes required changes to the
test case (now that those have been committed), and some re-wording in
the docs, per Joe's suggestion. All the tests pass here.
-# Most users use SCRAM authentication, but some users use older clients -# that don't support SCRAM authentication, and need to be able to log -# in using MD5 authentication. Such users are put in the @md5users -# group, everyone else must use SCRAM. +# Require SCRAM authentication for most users, but make an exception +# for user 'mike', who uses an older client that doesn't support SCRAM +# authentication. # # TYPE DATABASE USER ADDRESS METHOD -host all @md5users .example.com md5 +host all mike .example.com md5 Why not still using @md5users?
The old example didn't make much sense, now that md5 means "md5 or
scram". Could've still used @md5users, but I think this is more clear.
The old explanation was wrong or at least misleading anyway, because
@md5users doesn't refer to a group, but a flat file that lists roles.
- Heikki
Attachments:
0001-Allow-SCRAM-authentication-when-pg_hba.conf-says-md5-2.patchapplication/x-download; name=0001-Allow-SCRAM-authentication-when-pg_hba.conf-says-md5-2.patchDownload
From 06383d8f2a1538ffb62ec5796dfe1e09b798587d Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Wed, 22 Mar 2017 13:49:10 +0200
Subject: [PATCH 1/1] 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. In the
worst case, 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 switched to SCRAM 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.
---
doc/src/sgml/client-auth.sgml | 37 ++++---
src/backend/libpq/auth-scram.c | 104 ++++++++++--------
src/backend/libpq/auth.c | 173 +++++++++++++++++++-----------
src/backend/libpq/crypt.c | 44 +++-----
src/backend/libpq/hba.c | 2 +-
src/include/libpq/crypt.h | 3 +-
src/include/libpq/hba.h | 2 +-
src/include/libpq/scram.h | 2 +-
src/test/authentication/t/001_password.pl | 6 +-
9 files changed, 209 insertions(+), 164 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index bbd52a5418..9050be44d3 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -412,23 +412,22 @@ hostnossl <replaceable>database</replaceable> <replaceable>user</replaceable>
</varlistentry>
<varlistentry>
- <term><literal>md5</></term>
+ <term><literal>scram</></term>
<listitem>
<para>
- Require the client to supply a double-MD5-hashed password for
- authentication.
- See <xref linkend="auth-password"> for details.
+ Perform SCRAM-SHA-256 authentication to verify the user's
+ password. See <xref linkend="auth-password"> for details.
</para>
</listitem>
</varlistentry>
<varlistentry>
- <term><literal>scram</></term>
+ <term><literal>md5</></term>
<listitem>
<para>
- Perform SCRAM-SHA-256 authentication to verify the user's
- password.
- See <xref linkend="auth-password"> for details.
+ Perform SCRAM-SHA-256 or MD5 authentication to verify the
+ user's password. See <xref linkend="auth-password">
+ for details.
</para>
</listitem>
</varlistentry>
@@ -689,13 +688,12 @@ host postgres all 192.168.12.10/32 scram
# Allow any user from hosts in the example.com domain to connect to
# any database if the user's password is correctly supplied.
#
-# Most users use SCRAM authentication, but some users use older clients
-# that don't support SCRAM authentication, and need to be able to log
-# in using MD5 authentication. Such users are put in the @md5users
-# group, everyone else must use SCRAM.
+# Require SCRAM authentication for most users, but make an exception
+# for user 'mike', who uses an older client that doesn't support SCRAM
+# authentication.
#
# TYPE DATABASE USER ADDRESS METHOD
-host all @md5users .example.com md5
+host all mike .example.com md5
host all all .example.com scram
# In the absence of preceding "host" lines, these two lines will
@@ -949,12 +947,13 @@ omicron bryanh guest1
</para>
<para>
- In <literal>md5</>, the client sends a hash of a random challenge,
- generated by the server, and the password. It prevents password sniffing,
- but is less secure than <literal>scram</>, and provides no protection
- if an attacker manages to steal the password hash from the server.
- <literal>md5</> cannot be used with the <xref
- linkend="guc-db-user-namespace"> feature.
+ <literal>md5</> allows falling back to a less secure challenge-response
+ mechanism for those users with an MD5 hashed password.
+ The fallback mechanism also prevents password sniffing, but provides no
+ protection if an attacker manages to steal the password hash from the
+ server, and it cannot be used with the <xref
+ linkend="guc-db-user-namespace"> feature. For all other users,
+ <literal>md5</> works the same as <literal>scram</>.
</para>
<para>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index db15a2fac6..bcc8d03ef5 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -130,79 +130,91 @@ static char *scram_MockSalt(const char *username);
* after the beginning of the exchange with verifier data.
*
* 'username' is the provided by the client. 'shadow_pass' is the role's
- * password verifier, from pg_authid.rolpassword. If 'doomed' is true, the
- * authentication must fail, as if an incorrect password was given.
- * 'shadow_pass' may be NULL, when 'doomed' is set.
+ * password verifier, from pg_authid.rolpassword. If 'shadow_pass' is NULL, we
+ * still perform an authentication exchange, but it will fail, as if an
+ * incorrect password was given.
*/
void *
-pg_be_scram_init(const char *username, const char *shadow_pass, bool doomed)
+pg_be_scram_init(const char *username, const char *shadow_pass)
{
scram_state *state;
- int password_type;
+ bool got_verifier;
state = (scram_state *) palloc0(sizeof(scram_state));
state->state = SCRAM_AUTH_INIT;
state->username = username;
/*
- * Perform sanity checks on the provided password after catalog lookup.
- * The authentication is bound to fail if the lookup itself failed or if
- * the password stored is MD5-encrypted. Authentication is possible for
- * users with a valid plain password though.
+ * Parse the stored password verifier.
*/
+ if (shadow_pass)
+ {
+ int password_type = get_password_type(shadow_pass);
- if (shadow_pass == NULL || doomed)
- password_type = -1;
- else
- password_type = get_password_type(shadow_pass);
+ if (password_type == PASSWORD_TYPE_SCRAM)
+ {
+ if (parse_scram_verifier(shadow_pass, &state->salt, &state->iterations,
+ state->StoredKey, state->ServerKey))
+ got_verifier = true;
+ else
+ {
+ /*
+ * The password looked like a SCRAM verifier, but could not be
+ * parsed.
+ */
+ elog(LOG, "invalid SCRAM verifier for user \"%s\"", username);
+ got_verifier = false;
+ }
+ }
+ else if (password_type == PASSWORD_TYPE_PLAINTEXT)
+ {
+ /*
+ * The stored password is in plain format. Generate a fresh SCRAM
+ * verifier from it, and proceed with that.
+ */
+ char *verifier;
- if (password_type == PASSWORD_TYPE_SCRAM)
- {
- if (!parse_scram_verifier(shadow_pass, &state->salt, &state->iterations,
- state->StoredKey, state->ServerKey))
+ verifier = scram_build_verifier(username, shadow_pass, 0);
+
+ (void) parse_scram_verifier(verifier, &state->salt, &state->iterations,
+ state->StoredKey, state->ServerKey);
+ pfree(verifier);
+
+ got_verifier = true;
+ }
+ else
{
/*
- * The password looked like a SCRAM verifier, but could not be
- * parsed.
+ * The user doesn't have SCRAM verifier, nor could we generate
+ * one. (You cannot do SCRAM authentication with an MD5 hash.)
*/
- elog(LOG, "invalid SCRAM verifier for user \"%s\"", username);
- doomed = true;
+ state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."),
+ state->username);
+ got_verifier = false;
}
}
- else if (password_type == PASSWORD_TYPE_PLAINTEXT)
+ else
{
- char *verifier;
-
/*
- * The password provided is in plain format, in which case a fresh
- * SCRAM verifier can be generated and used for the rest of the
- * processing.
+ * The caller requested us to perform a dummy authentication. This is
+ * considered normal, since the caller requested it, so don't set log
+ * detail.
*/
- verifier = scram_build_verifier(username, shadow_pass, 0);
-
- (void) parse_scram_verifier(verifier, &state->salt, &state->iterations,
- state->StoredKey, state->ServerKey);
- pfree(verifier);
+ got_verifier = false;
}
- else
- doomed = true;
- if (doomed)
+ /*
+ * If the user did not have a valid SCRAM verifier, we still go through
+ * the motions with a mock one, and fail as if the client supplied an
+ * incorrect password. This is to avoid revealing information to an
+ * attacker.
+ */
+ if (!got_verifier)
{
- /*
- * We don't have a valid SCRAM verifier, nor could we generate one, or
- * the caller requested us to perform a dummy authentication.
- *
- * The authentication is bound to fail, but to avoid revealing
- * information to the attacker, go through the motions with a fake
- * SCRAM verifier, and fail as if the password was incorrect.
- */
- state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."),
- state->username);
mock_scram_verifier(username, &state->salt, &state->iterations,
state->StoredKey, state->ServerKey);
+ state->doomed = true;
}
- state->doomed = doomed;
return state;
}
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index ebf10bbbae..27ed11a554 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -24,6 +24,7 @@
#include <sys/select.h>
#endif
+#include "commands/user.h"
#include "common/ip.h"
#include "common/md5.h"
#include "libpq/auth.h"
@@ -49,17 +50,15 @@ static char *recv_password_packet(Port *port);
/*----------------------------------------------------------------
- * MD5 authentication
+ * Password-based authentication methods (password, md5, and scram)
*----------------------------------------------------------------
*/
-static int CheckMD5Auth(Port *port, char **logdetail);
+static int CheckPasswordAuth(Port *port, char **logdetail);
+static int CheckPWChallengeAuth(Port *port, char **logdetail);
-/*----------------------------------------------------------------
- * Plaintext password authentication
- *----------------------------------------------------------------
- */
+static int CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail);
+static int CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail);
-static int CheckPasswordAuth(Port *port, char **logdetail);
/*----------------------------------------------------------------
* Ident authentication
@@ -199,12 +198,6 @@ static int pg_SSPI_make_upn(char *accountname,
static int CheckRADIUSAuth(Port *port);
-/*----------------------------------------------------------------
- * SASL authentication
- *----------------------------------------------------------------
- */
-static int CheckSASLAuth(Port *port, char **logdetail);
-
/*
* Maximum accepted size of GSS and SSPI authentication tokens.
*
@@ -290,7 +283,7 @@ auth_failed(Port *port, int status, char *logdetail)
break;
case uaPassword:
case uaMD5:
- case uaSASL:
+ case uaSCRAM:
errstr = gettext_noop("password authentication failed for user \"%s\"");
/* We use it to indicate if a .pgpass password failed. */
errcode_return = ERRCODE_INVALID_PASSWORD;
@@ -551,17 +544,14 @@ ClientAuthentication(Port *port)
break;
case uaMD5:
- status = CheckMD5Auth(port, &logdetail);
+ case uaSCRAM:
+ status = CheckPWChallengeAuth(port, &logdetail);
break;
case uaPassword:
status = CheckPasswordAuth(port, &logdetail);
break;
- case uaSASL:
- status = CheckSASLAuth(port, &logdetail);
- break;
-
case uaPAM:
#ifdef USE_PAM
status = CheckPAMAuth(port, port->user_name, "");
@@ -709,41 +699,30 @@ recv_password_packet(Port *port)
/*----------------------------------------------------------------
- * MD5 authentication
+ * Password-based authentication mechanisms
*----------------------------------------------------------------
*/
+/*
+ * Plaintext password authentication.
+ */
static int
-CheckMD5Auth(Port *port, char **logdetail)
+CheckPasswordAuth(Port *port, char **logdetail)
{
- char md5Salt[4]; /* Password salt */
char *passwd;
- char *shadow_pass;
int result;
+ char *shadow_pass;
- if (Db_user_namespace)
- ereport(FATAL,
- (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
- errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled")));
-
- /* include the salt to use for computing the response */
- if (!pg_backend_random(md5Salt, 4))
- {
- ereport(LOG,
- (errmsg("could not generate random MD5 salt")));
- return STATUS_ERROR;
- }
-
- sendAuthRequest(port, AUTH_REQ_MD5, md5Salt, 4);
+ sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
passwd = recv_password_packet(port);
if (passwd == NULL)
return STATUS_EOF; /* client wouldn't send password */
- result = get_role_password(port->user_name, &shadow_pass, logdetail);
+ shadow_pass = get_role_password(port->user_name, logdetail);
if (result == STATUS_OK)
- result = md5_crypt_verify(port->user_name, shadow_pass, passwd,
- md5Salt, 4, logdetail);
+ result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
+ logdetail);
if (shadow_pass)
pfree(shadow_pass);
@@ -752,42 +731,114 @@ CheckMD5Auth(Port *port, char **logdetail)
return result;
}
-/*----------------------------------------------------------------
- * Plaintext password authentication
- *----------------------------------------------------------------
+/*
+ * MD5 and SCRAM authentication.
*/
+static int
+CheckPWChallengeAuth(Port *port, char **logdetail)
+{
+ int auth_result;
+ char *shadow_pass;
+ PasswordType pwtype;
+
+ Assert(port->hba->auth_method == uaSCRAM ||
+ port->hba->auth_method == uaMD5);
+
+ /* First look up the user's password. */
+ shadow_pass = get_role_password(port->user_name, logdetail);
+
+ /*
+ * If the user does not exist, or has no password, we still go through the
+ * motions of authentication, to avoid revealing to the client that the
+ * user didn't exist. If 'md5' is allowed, we choose whether to use 'md5'
+ * or 'scram' authentication based on current password_encryption setting.
+ * The idea is that most genuine users probably have a password of that
+ * type, if we pretend that this user had a password of that type, too, it
+ * "blends in" best.
+ *
+ * If the user had a password, but it was expired, we'll use the details
+ * of the expired password for the authentication, but report it as
+ * failure to the client even if correct password was given.
+ */
+ if (!shadow_pass)
+ pwtype = Password_encryption;
+ else
+ pwtype = get_password_type(shadow_pass);
+
+ /*
+ * If 'md5' authentication is allowed, decide whether to perform 'md5' or
+ * 'scram' authentication based on the type of password the user has. If
+ * it's an MD5 hash, we must do MD5 authentication, and if it's a SCRAM
+ * verifier, we must do SCRAM authentication. If it's stored in
+ * plaintext, we could do either one, so we opt for the more secure
+ * mechanism, SCRAM.
+ *
+ * If MD5 authentication is not allowed, always use SCRAM. If the user
+ * had an MD5 password, CheckSCRAMAuth() will fail.
+ */
+ if (port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5)
+ {
+ auth_result = CheckMD5Auth(port, shadow_pass, logdetail);
+ }
+ else
+ {
+ auth_result = CheckSCRAMAuth(port, shadow_pass, logdetail);
+ }
+
+ if (shadow_pass)
+ pfree(shadow_pass);
+
+ /*
+ * If get_role_password() returned error, return error, even if the
+ * authentication succeeded.
+ */
+ if (!shadow_pass)
+ {
+ Assert(auth_result != STATUS_OK);
+ return STATUS_ERROR;
+ }
+ return auth_result;
+}
static int
-CheckPasswordAuth(Port *port, char **logdetail)
+CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail)
{
+ char md5Salt[4]; /* Password salt */
char *passwd;
int result;
- char *shadow_pass;
- sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
+ if (Db_user_namespace)
+ ereport(FATAL,
+ (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
+ errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled")));
+
+ /* include the salt to use for computing the response */
+ if (!pg_backend_random(md5Salt, 4))
+ {
+ ereport(LOG,
+ (errmsg("could not generate random MD5 salt")));
+ return STATUS_ERROR;
+ }
+
+ sendAuthRequest(port, AUTH_REQ_MD5, md5Salt, 4);
passwd = recv_password_packet(port);
if (passwd == NULL)
return STATUS_EOF; /* client wouldn't send password */
- result = get_role_password(port->user_name, &shadow_pass, logdetail);
- if (result == STATUS_OK)
- result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
- logdetail);
-
if (shadow_pass)
- pfree(shadow_pass);
+ result = md5_crypt_verify(port->user_name, shadow_pass, passwd,
+ md5Salt, 4, logdetail);
+ else
+ result = STATUS_ERROR;
+
pfree(passwd);
return result;
}
-/*----------------------------------------------------------------
- * SASL authentication system
- *----------------------------------------------------------------
- */
static int
-CheckSASLAuth(Port *port, char **logdetail)
+CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
{
int mtype;
StringInfoData buf;
@@ -795,8 +846,6 @@ CheckSASLAuth(Port *port, char **logdetail)
char *output = NULL;
int outputlen = 0;
int result;
- char *shadow_pass;
- bool doomed = false;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -826,11 +875,9 @@ CheckSASLAuth(Port *port, char **logdetail)
* This is because we don't want to reveal to an attacker what usernames
* are valid, nor which users have a valid password.
*/
- if (get_role_password(port->user_name, &shadow_pass, logdetail) != STATUS_OK)
- doomed = true;
/* Initialize the status tracker for message exchanges */
- scram_opaq = pg_be_scram_init(port->user_name, shadow_pass, doomed);
+ scram_opaq = pg_be_scram_init(port->user_name, shadow_pass);
/*
* Loop through SASL message exchange. This exchange can consist of
@@ -874,7 +921,7 @@ CheckSASLAuth(Port *port, char **logdetail)
*/
result = pg_be_scram_exchange(scram_opaq, buf.data, buf.len,
&output, &outputlen,
- doomed ? NULL : logdetail);
+ logdetail);
/* input buffer no longer used */
pfree(buf.data);
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index ac10751ec2..34beab5334 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -31,25 +31,18 @@
/*
* Fetch stored password for a user, for authentication.
*
- * Returns STATUS_OK on success. On error, returns STATUS_ERROR, and stores
- * a palloc'd string describing the reason, for the postmaster log, in
- * *logdetail. The error reason should *not* be sent to the client, to avoid
- * giving away user information!
- *
- * If the password is expired, it is still returned in *shadow_pass, but the
- * return code is STATUS_ERROR. On other errors, *shadow_pass is set to
- * NULL.
+ * On error, returns NULL, and stores a palloc'd string describing the reason,
+ * for the postmaster log, in *logdetail. The error reason should *not* be
+ * sent to the client, to avoid giving away user information!
*/
-int
-get_role_password(const char *role, char **shadow_pass, char **logdetail)
+char *
+get_role_password(const char *role, char **logdetail)
{
- int retval = STATUS_ERROR;
TimestampTz vuntil = 0;
HeapTuple roleTup;
Datum datum;
bool isnull;
-
- *shadow_pass = NULL;
+ char *shadow_pass;
/* Get role info from pg_authid */
roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -57,7 +50,7 @@ get_role_password(const char *role, char **shadow_pass, char **logdetail)
{
*logdetail = psprintf(_("Role \"%s\" does not exist."),
role);
- return STATUS_ERROR; /* no such user */
+ return NULL; /* no such user */
}
datum = SysCacheGetAttr(AUTHNAME, roleTup,
@@ -67,9 +60,9 @@ get_role_password(const char *role, char **shadow_pass, char **logdetail)
ReleaseSysCache(roleTup);
*logdetail = psprintf(_("User \"%s\" has no password assigned."),
role);
- return STATUS_ERROR; /* user has no password */
+ return NULL; /* user has no password */
}
- *shadow_pass = TextDatumGetCString(datum);
+ shadow_pass = TextDatumGetCString(datum);
datum = SysCacheGetAttr(AUTHNAME, roleTup,
Anum_pg_authid_rolvaliduntil, &isnull);
@@ -78,30 +71,25 @@ get_role_password(const char *role, char **shadow_pass, char **logdetail)
ReleaseSysCache(roleTup);
- if (**shadow_pass == '\0')
+ if (*shadow_pass == '\0')
{
*logdetail = psprintf(_("User \"%s\" has an empty password."),
role);
- pfree(*shadow_pass);
- *shadow_pass = NULL;
- return STATUS_ERROR; /* empty password */
+ pfree(shadow_pass);
+ return NULL; /* empty password */
}
/*
- * Password OK, now check to be sure we are not past rolvaliduntil
+ * Password OK, but check to be sure we are not past rolvaliduntil
*/
- if (isnull)
- retval = STATUS_OK;
- else if (vuntil < GetCurrentTimestamp())
+ if (!isnull && vuntil < GetCurrentTimestamp())
{
*logdetail = psprintf(_("User \"%s\" has an expired password."),
role);
- retval = STATUS_ERROR;
+ return NULL;
}
- else
- retval = STATUS_OK;
- return retval;
+ return shadow_pass;
}
/*
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 3817d249c4..e2c1a86901 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1325,7 +1325,7 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
parsedline->auth_method = uaMD5;
}
else if (strcmp(token->string, "scram") == 0)
- parsedline->auth_method = uaSASL;
+ parsedline->auth_method = uaSCRAM;
else if (strcmp(token->string, "pam") == 0)
#ifdef USE_PAM
parsedline->auth_method = uaPAM;
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index 0502d6a0e5..3b5da69b08 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -32,8 +32,7 @@ extern PasswordType get_password_type(const char *shadow_pass);
extern char *encrypt_password(PasswordType target_type, const char *role,
const char *password);
-extern int get_role_password(const char *role, char **shadow_pass,
- char **logdetail);
+extern char *get_role_password(const char *role, char **logdetail);
extern int md5_crypt_verify(const char *role, const char *shadow_pass,
const char *client_pass, const char *md5_salt,
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f55edb16a..4925e9d609 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -30,7 +30,7 @@ typedef enum UserAuth
uaIdent,
uaPassword,
uaMD5,
- uaSASL,
+ uaSCRAM,
uaGSS,
uaSSPI,
uaPAM,
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index fb21e056c8..e373f0c07e 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -22,7 +22,7 @@
#define SASL_EXCHANGE_FAILURE 2
/* Routines dedicated to authentication */
-extern void *pg_be_scram_init(const char *username, const char *shadow_pass, bool doomed);
+extern void *pg_be_scram_init(const char *username, const char *shadow_pass);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 8726a23e0d..d7bc13bd58 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -75,10 +75,10 @@ SKIP:
test_role($node, 'md5_role', 'scram', 2);
test_role($node, 'plain_role', 'scram', 0);
- # For "md5" method, users "plain_role" and "md5_role" should be able to
- # connect.
+ # For "md5" method, all users should be able to connect (SCRAM
+ # authentication will be performed for the user with a scram verifier.)
reset_pg_hba($node, 'md5');
- test_role($node, 'scram_role', 'md5', 2);
+ test_role($node, 'scram_role', 'md5', 0);
test_role($node, 'md5_role', 'md5', 0);
test_role($node, 'plain_role', 'md5', 0);
}
--
2.11.0
On Wed, Mar 22, 2017 at 8:54 PM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 03/17/2017 05:38 AM, Michael Paquier wrote:
Regression tests are proving to be useful here (it would be nice to
get those committed first!). I am noticing that this patch breaks
connection for users with cleartext or md5-hashed verifier when
"password" is used in pg_hba.conf.Are you sure? It works for me.
Hm... All the tests of password are still broken for me on macos, but
work on Linux:
not ok 4 - authentication success for method password, role scram_role
# Failed test 'authentication success for method password, role scram_role'
# at t/001_password.pl line 39.
# got: '2'
# expected: '0'
not ok 5 - authentication success for method password, role md5_role
# Failed test 'authentication success for method password, role md5_role'
# at t/001_password.pl line 39.
# got: '2'
# expected: '0'
not ok 6 - authentication success for method password, role plain_role
[... dig ... dig ...]
And after a lookup the failure is here:
- result = get_role_password(port->user_name, &shadow_pass, logdetail);
+ shadow_pass = get_role_password(port->user_name, logdetail);
if (result == STATUS_OK)
result is never setup in this code path, so that may crash.
And you need to do something like that, which makes the tests pass here:
@@ -721,6 +721,7 @@ CheckPasswordAuth(Port *port, char **logdetail)
return STATUS_EOF; /* client wouldn't send password */
shadow_pass = get_role_password(port->user_name, logdetail);
+ result = shadow_pass != NULL ? STATUS_OK : STATUS_ERROR;
if (result == STATUS_OK)
result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
logdetail);
Here's a slightly updated patch that includes required changes to the test
case (now that those have been committed), and some re-wording in the docs,
per Joe's suggestion. All the tests pass here.
+ verifier = scram_build_verifier(username, shadow_pass, 0);
+
+ (void) parse_scram_verifier(verifier, &state->salt,
&state->iterations,
+ state->StoredKey, state->ServerKey);
+ pfree(verifier);
Not directly a problem of this patch, but scram_build_verifier can return NULL.
-# Most users use SCRAM authentication, but some users use older clients -# that don't support SCRAM authentication, and need to be able to log -# in using MD5 authentication. Such users are put in the @md5users -# group, everyone else must use SCRAM. +# Require SCRAM authentication for most users, but make an exception +# for user 'mike', who uses an older client that doesn't support SCRAM +# authentication. # # TYPE DATABASE USER ADDRESS METHOD -host all @md5users .example.com md5 +host all mike .example.com md5 Why not still using @md5users?The old example didn't make much sense, now that md5 means "md5 or scram".
Could've still used @md5users, but I think this is more clear. The old
explanation was wrong or at least misleading anyway, because @md5users
doesn't refer to a group, but a flat file that lists roles.
This patch introduces for the first time a non-generic user name in
pg_hba.conf, that's why, keeping in mind that users could just
copy-paste what is in the docs to make their own file, the approach of
using an @ marker looks more generic to me. But I won't insist on this
point more.
I like the move of removing the status error codes from
get_role_password(). With this support grid, only users with
MD5-hashed verifiers cannot login when the matching hba entry uses
scram.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/23/2017 06:41 AM, Michael Paquier wrote:
And after a lookup the failure is here: - result = get_role_password(port->user_name, &shadow_pass, logdetail); + shadow_pass = get_role_password(port->user_name, logdetail); if (result == STATUS_OK) result is never setup in this code path, so that may crash.
Ah, of course. For some reason, I has -Wno-maybe-uninitialized in my
configure command line. Without that, gcc even warns about that.
Fixed, and pushed. Thanks!
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Mar 24, 2017 at 8:36 PM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 03/23/2017 06:41 AM, Michael Paquier wrote:
And after a lookup the failure is here: - result = get_role_password(port->user_name, &shadow_pass, logdetail); + shadow_pass = get_role_password(port->user_name, logdetail); if (result == STATUS_OK) result is never setup in this code path, so that may crash.Ah, of course. For some reason, I has -Wno-maybe-uninitialized in my
configure command line. Without that, gcc even warns about that.Fixed, and pushed. Thanks!
OK, cool.
In order to close this thread, I propose to reuse the patches I sent
here to make scram_build_verifier() available to frontends:
/messages/by-id/CAB7nPqT4yc3u8wspYkWbG088Ndp6asMH3=Zb___Ck89CTvziYQ@mail.gmail.com
And on top of it modify \password so as it generates a md5 verifier
for pre-9.6 servers and a scram one for post-10 servers by looking at
the backend version of the current connection. What do you think?
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 03/24/2017 03:02 PM, Michael Paquier wrote:
In order to close this thread, I propose to reuse the patches I sent
here to make scram_build_verifier() available to frontends:
/messages/by-id/CAB7nPqT4yc3u8wspYkWbG088Ndp6asMH3=Zb___Ck89CTvziYQ@mail.gmail.comAnd on top of it modify \password so as it generates a md5 verifier
for pre-9.6 servers and a scram one for post-10 servers by looking at
the backend version of the current connection. What do you think?
Yep, sounds like a plan.
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Mar 24, 2017 at 10:12 PM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 03/24/2017 03:02 PM, Michael Paquier wrote:
In order to close this thread, I propose to reuse the patches I sent
here to make scram_build_verifier() available to frontends:/messages/by-id/CAB7nPqT4yc3u8wspYkWbG088Ndp6asMH3=Zb___Ck89CTvziYQ@mail.gmail.com
And on top of it modify \password so as it generates a md5 verifier
for pre-9.6 servers and a scram one for post-10 servers by looking at
the backend version of the current connection. What do you think?Yep, sounds like a plan.
And attached is a set of rebased patches, with createuser and psql's
\password extended to do that.
--
Michael
Attachments:
0001-Use-base64-based-encoding-for-stored-and-server-keys.patchtext/x-patch; charset=US-ASCII; name=0001-Use-base64-based-encoding-for-stored-and-server-keys.patchDownload
From 21e73b1bc1cf1985b7ca0e638b0e752ffd4b89b1 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Sat, 25 Mar 2017 13:31:38 +0900
Subject: [PATCH 1/5] Use base64-based encoding for stored and server keys in
SCRAM verifiers
In order to be able to generate a SCRAM verifier even for frontends, let's
simplify the tools used to generate it and switch all the elements of the
verifiers to be base64-encoded using the routines already in place in
src/common/.
---
doc/src/sgml/catalogs.sgml | 2 +-
src/backend/libpq/auth-scram.c | 30 +++++++++++++++++-------------
src/test/regress/expected/password.out | 8 ++++----
src/test/regress/sql/password.sql | 8 ++++----
4 files changed, 26 insertions(+), 22 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index ac39c639ed..30a288ab91 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1371,7 +1371,7 @@
identify the password as a SCRAM-SHA-256 verifier. The second field is a
salt, Base64-encoded, and the third field is the number of iterations used
to generate the password. The fourth field and fifth field are the stored
- key and server key, respectively, in hexadecimal format. A password that
+ key and server key, respectively, in Base64 format. A password that
does not follow either of those formats is assumed to be unencrypted.
</para>
</sect1>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index bcc8d03ef5..e8068d3b7a 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -339,8 +339,8 @@ scram_build_verifier(const char *username, const char *password,
int iterations)
{
uint8 keybuf[SCRAM_KEY_LEN + 1];
- char storedkey_hex[SCRAM_KEY_LEN * 2 + 1];
- char serverkey_hex[SCRAM_KEY_LEN * 2 + 1];
+ char *encoded_storedkey;
+ char *encoded_serverkey;
char salt[SCRAM_SALT_LEN];
char *encoded_salt;
int encoded_len;
@@ -360,20 +360,25 @@ scram_build_verifier(const char *username, const char *password,
encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt);
encoded_salt[encoded_len] = '\0';
- /* Calculate StoredKey, and encode it in hex */
+ /* Calculate StoredKey, and encode it in base64 */
+ encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
- (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, storedkey_hex);
- storedkey_hex[SCRAM_KEY_LEN * 2] = '\0';
+ encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
+ encoded_storedkey);
+ encoded_storedkey[encoded_len] = '\0';
/* And same for ServerKey */
+ encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
SCRAM_SERVER_KEY_NAME, keybuf);
- (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, serverkey_hex);
- serverkey_hex[SCRAM_KEY_LEN * 2] = '\0';
+ encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
+ encoded_serverkey);
+ encoded_serverkey[encoded_len] = '\0';
- return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations, storedkey_hex, serverkey_hex);
+ return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations,
+ encoded_storedkey, encoded_serverkey);
}
/*
@@ -483,17 +488,16 @@ parse_scram_verifier(const char *verifier, char **salt, int *iterations,
/* storedkey */
if ((p = strtok(NULL, ":")) == NULL)
goto invalid_verifier;
- if (strlen(p) != SCRAM_KEY_LEN * 2)
+ if (strlen(p) != pg_b64_enc_len(SCRAM_KEY_LEN) - 1)
goto invalid_verifier;
-
- hex_decode(p, SCRAM_KEY_LEN * 2, (char *) stored_key);
+ pg_b64_decode(p, pg_b64_enc_len(SCRAM_KEY_LEN), (char *) stored_key);
/* serverkey */
if ((p = strtok(NULL, ":")) == NULL)
goto invalid_verifier;
- if (strlen(p) != SCRAM_KEY_LEN * 2)
+ if (strlen(p) != pg_b64_enc_len(SCRAM_KEY_LEN) - 1)
goto invalid_verifier;
- hex_decode(p, SCRAM_KEY_LEN * 2, (char *) server_key);
+ pg_b64_decode(p, pg_b64_enc_len(SCRAM_KEY_LEN), (char *) server_key);
pfree(v);
return true;
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index c503e43abe..0cdb6141e2 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -23,11 +23,11 @@ CREATE ROLE regress_passwd5 PASSWORD NULL;
-- check list of created entries
--
-- The scram verifier will look something like:
--- scram-sha-256:E4HxLGtnRzsYwg==:4096:5ebc825510cb7862efd87dfa638d8337179e6913a724441dc9e888a856fbc10c:e966b1c72fad89d69aaebb156eae04edc9581286f92207c044711e79cd461bee
+-- scram-sha-256:E4HxLGtnRzsYwg==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo=
--
-- Since the salt is random, the exact value stored will be different on every test
-- run. Use a regular expression to mask the changing parts.
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
@@ -59,11 +59,11 @@ ALTER ROLE regress_passwd1 UNENCRYPTED PASSWORD 'foo'; -- unencrypted
ALTER ROLE regress_passwd2 UNENCRYPTED PASSWORD 'md5dfa155cadd5f4ad57860162f3fab9cdb'; -- encrypted with MD5
SET password_encryption = 'md5';
ALTER ROLE regress_passwd3 ENCRYPTED PASSWORD 'foo'; -- encrypted with MD5
-ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:3ded2376f7aafa93b1bdbd71bcc18b7d6ee50ed018029cc583d152ef3fc7d430:a6dd36dfc94c181956a6ae95f05e01b1864f0a22a2657d1de4ba84d2a24dc438'; -- client-supplied SCRAM verifier, use as it is
+ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; -- client-supplied SCRAM verifier, use as it is
SET password_encryption = 'scram';
ALTER ROLE regress_passwd5 ENCRYPTED PASSWORD 'foo'; -- create SCRAM verifier
CREATE ROLE regress_passwd6 ENCRYPTED PASSWORD 'md53725413363ab045e20521bf36b8d8d7f'; -- encrypted with MD5, use as it is
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql
index f4b3a9ac3a..df1ff989cc 100644
--- a/src/test/regress/sql/password.sql
+++ b/src/test/regress/sql/password.sql
@@ -24,11 +24,11 @@ CREATE ROLE regress_passwd5 PASSWORD NULL;
-- check list of created entries
--
-- The scram verifier will look something like:
--- scram-sha-256:E4HxLGtnRzsYwg==:4096:5ebc825510cb7862efd87dfa638d8337179e6913a724441dc9e888a856fbc10c:e966b1c72fad89d69aaebb156eae04edc9581286f92207c044711e79cd461bee
+-- scram-sha-256:E4HxLGtnRzsYwg==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo=
--
-- Since the salt is random, the exact value stored will be different on every test
-- run. Use a regular expression to mask the changing parts.
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
@@ -48,13 +48,13 @@ ALTER ROLE regress_passwd2 UNENCRYPTED PASSWORD 'md5dfa155cadd5f4ad57860162f3fab
SET password_encryption = 'md5';
ALTER ROLE regress_passwd3 ENCRYPTED PASSWORD 'foo'; -- encrypted with MD5
-ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:3ded2376f7aafa93b1bdbd71bcc18b7d6ee50ed018029cc583d152ef3fc7d430:a6dd36dfc94c181956a6ae95f05e01b1864f0a22a2657d1de4ba84d2a24dc438'; -- client-supplied SCRAM verifier, use as it is
+ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; -- client-supplied SCRAM verifier, use as it is
SET password_encryption = 'scram';
ALTER ROLE regress_passwd5 ENCRYPTED PASSWORD 'foo'; -- create SCRAM verifier
CREATE ROLE regress_passwd6 ENCRYPTED PASSWORD 'md53725413363ab045e20521bf36b8d8d7f'; -- encrypted with MD5, use as it is
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
--
2.12.1
0003-Move-routine-to-build-SCRAM-verifier-into-src-common.patchtext/x-patch; charset=US-ASCII; name=0003-Move-routine-to-build-SCRAM-verifier-into-src-common.patchDownload
From ea4884552ed7646973c4be5f3357a6acb806f95c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Sat, 25 Mar 2017 13:42:45 +0900
Subject: [PATCH 3/5] Move routine to build SCRAM verifier into src/common/
This is aimed at being used by libpq to allow frontend-side creation
of SCRAM verifiers. The result is not anymore allocated by the routine
itself, the caller is responsible for that, similarly to md5.
---
src/backend/libpq/auth-scram.c | 56 ++---------------------------
src/backend/libpq/crypt.c | 7 +++-
src/common/scram-common.c | 74 +++++++++++++++++++++++++++++++++++++++
src/include/common/scram-common.h | 20 +++++++++++
src/include/libpq/scram.h | 5 +--
5 files changed, 103 insertions(+), 59 deletions(-)
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index e8068d3b7a..9cec3c0f82 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -172,9 +172,9 @@ pg_be_scram_init(const char *username, const char *shadow_pass)
* The stored password is in plain format. Generate a fresh SCRAM
* verifier from it, and proceed with that.
*/
- char *verifier;
+ char *verifier = palloc(SCRAM_VERIFIER_LEN + 1);
- verifier = scram_build_verifier(username, shadow_pass, 0);
+ (void) scram_build_verifier(username, shadow_pass, 0, verifier);
(void) parse_scram_verifier(verifier, &state->salt, &state->iterations,
state->StoredKey, state->ServerKey);
@@ -328,58 +328,6 @@ pg_be_scram_exchange(void *opaq, char *input, int inputlen,
return result;
}
-/*
- * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
- *
- * If iterations is 0, default number of iterations is used. The result is
- * palloc'd, so caller is responsible for freeing it.
- */
-char *
-scram_build_verifier(const char *username, const char *password,
- int iterations)
-{
- uint8 keybuf[SCRAM_KEY_LEN + 1];
- char *encoded_storedkey;
- char *encoded_serverkey;
- char salt[SCRAM_SALT_LEN];
- char *encoded_salt;
- int encoded_len;
-
- if (iterations <= 0)
- iterations = SCRAM_ITERATIONS_DEFAULT;
-
- if (!pg_backend_random(salt, SCRAM_SALT_LEN))
- {
- ereport(LOG,
- (errcode(ERRCODE_INTERNAL_ERROR),
- errmsg("could not generate random salt")));
- return NULL;
- }
-
- encoded_salt = palloc(pg_b64_enc_len(SCRAM_SALT_LEN) + 1);
- encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt);
- encoded_salt[encoded_len] = '\0';
-
- /* Calculate StoredKey, and encode it in base64 */
- encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
- scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
- iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
- scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
- encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
- encoded_storedkey);
- encoded_storedkey[encoded_len] = '\0';
-
- /* And same for ServerKey */
- encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
- scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
- SCRAM_SERVER_KEY_NAME, keybuf);
- encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
- encoded_serverkey);
- encoded_serverkey[encoded_len] = '\0';
-
- return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations,
- encoded_storedkey, encoded_serverkey);
-}
/*
* Verify a plaintext password against a SCRAM verifier. This is used when
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 34beab5334..d9aa2d93d6 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,6 +20,7 @@
#include "catalog/pg_authid.h"
#include "common/md5.h"
+#include "common/scram-common.h"
#include "libpq/crypt.h"
#include "libpq/scram.h"
#include "miscadmin.h"
@@ -156,7 +157,11 @@ encrypt_password(PasswordType target_type, const char *role,
switch (guessed_type)
{
case PASSWORD_TYPE_PLAINTEXT:
- return scram_build_verifier(role, password, 0);
+ encrypted_password = palloc(SCRAM_VERIFIER_LEN + 1);
+ if (!scram_build_verifier(role, password, 0,
+ encrypted_password))
+ elog(ERROR, "password encryption failed");
+ return encrypted_password;
case PASSWORD_TYPE_MD5:
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index e44f38f652..00e45a800a 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -24,6 +24,11 @@
#include <arpa/inet.h>
#include "common/scram-common.h"
+#ifndef FRONTEND
+#include "utils/backend_random.h"
+#else
+#include "common/frontend_random.h"
+#endif
#define HMAC_IPAD 0x36
#define HMAC_OPAD 0x5C
@@ -184,3 +189,72 @@ scram_ClientOrServerKey(const char *password,
scram_HMAC_update(&ctx, keystr, strlen(keystr));
scram_HMAC_final(result, &ctx);
}
+
+/*
+ * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
+ *
+ * If iterations is 0, default number of iterations is used. The result is
+ * stored in "verifier" that caller is responsible to allocate a buffer of
+ * size SCRAM_VERIFIER_LEN. Returns true if the verifier has been generated,
+ * false otherwise. It is important for this routine to do no memory
+ * allocations.
+ */
+bool
+scram_build_verifier(const char *username, const char *password,
+ int iterations, char *verifier)
+{
+ uint8 keybuf[SCRAM_KEY_LEN + 1];
+ char salt[SCRAM_SALT_LEN];
+ char intbuf[SCRAM_ITERATION_LEN];
+ char *p;
+
+ if (iterations <= 0)
+ iterations = SCRAM_ITERATIONS_DEFAULT;
+
+#ifdef FRONTEND
+ if (!pg_frontend_random(salt, SCRAM_SALT_LEN))
+ return false;
+#else
+ if (!pg_backend_random(salt, SCRAM_SALT_LEN))
+ {
+ ereport(LOG,
+ (errcode(ERRCODE_INTERNAL_ERROR),
+ errmsg("could not generate random salt")));
+ return false;
+ }
+#endif
+
+ /* Fill in the data of the verifier */
+ p = verifier;
+ memcpy(p, SCRAM_VERIFIER_PREFIX, strlen(SCRAM_VERIFIER_PREFIX));
+ p += strlen(SCRAM_VERIFIER_PREFIX);
+ *p++ = ':';
+
+ /* salt */
+ (void) pg_b64_encode(salt, SCRAM_SALT_LEN, p);
+ p += pg_b64_enc_len(SCRAM_SALT_LEN);
+ *p++ = ':';
+
+ /* iterations */
+ sprintf(intbuf, "%d", iterations);
+ memcpy(p, intbuf, strlen(intbuf));
+ p += strlen(intbuf);
+ *p++ = ':';
+
+ /* Calculate StoredKey, and encode it in base64 */
+ scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
+ iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
+ scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
+ (void) pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, p);
+ p += pg_b64_enc_len(SCRAM_KEY_LEN) - 1;
+ *p++ = ':';
+
+ /* And same for ServerKey */
+ scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
+ SCRAM_SERVER_KEY_NAME, keybuf);
+ (void) pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, p);
+ p += pg_b64_enc_len(SCRAM_KEY_LEN) - 1;
+ *p++ = '\0';
+
+ return true;
+}
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index 7c98cc74d6..3fdfca44d6 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -13,6 +13,7 @@
#ifndef SCRAM_COMMON_H
#define SCRAM_COMMON_H
+#include "common/base64.h"
#include "common/sha2.h"
/* Length of SCRAM keys (client and server) */
@@ -41,6 +42,23 @@
#define SCRAM_SERVER_KEY_NAME "Server Key"
#define SCRAM_CLIENT_KEY_NAME "Client Key"
+#define SCRAM_VERIFIER_PREFIX "scram-sha-256"
+
+/*
+ * Length of a SCRAM verifier, which is made of the following five fields
+ * separated by a colon:
+ * - prefix "scram-sha-256", made of 13 characters.
+ * - 4 colon separators.
+ * - 32-bit number of interations, up to 10 characters.
+ * - base64-encoded salt of length SCRAM_SALT_LEN
+ * - base64-encoded stored key of length SCRAM_KEY_LEN
+ * - base64-encoded server key of length SCRAM_KEY_LEN
+ */
+#define SCRAM_VERIFIER_LEN (strlen("scram-sha-256") + 4 + \
+ SCRAM_ITERATION_LEN + \
+ pg_b64_enc_len(SCRAM_SALT_LEN) + \
+ pg_b64_enc_len(SCRAM_KEY_LEN) * 2)
+
/*
* Context data for HMAC used in SCRAM authentication.
*/
@@ -58,5 +76,7 @@ extern void scram_H(const uint8 *str, int len, uint8 *result);
extern void scram_ClientOrServerKey(const char *password, const char *salt,
int saltlen, int iterations,
const char *keystr, uint8 *result);
+extern bool scram_build_verifier(const char *username, const char *password,
+ int iterations, char *verifier);
#endif /* SCRAM_COMMON_H */
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index e373f0c07e..803fc4ef7a 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -26,10 +26,7 @@ extern void *pg_be_scram_init(const char *username, const char *shadow_pass);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
-/* Routines to handle and check SCRAM-SHA-256 verifier */
-extern char *scram_build_verifier(const char *username,
- const char *password,
- int iterations);
+/* Routines to check SCRAM-SHA-256 verifier */
extern bool is_scram_verifier(const char *verifier);
extern bool scram_verify_plain_password(const char *username,
const char *password, const char *verifier);
--
2.12.1
0004-Extend-PQencryptPassword-with-a-hashing-method.patchtext/x-patch; charset=US-ASCII; name=0004-Extend-PQencryptPassword-with-a-hashing-method.patchDownload
From 863c71ea02c1abe302aec622ed3ae7fb45c56425 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Sat, 25 Mar 2017 13:45:39 +0900
Subject: [PATCH 4/5] Extend PQencryptPassword with a hashing method
This extra argument can use the following values when hashing the
password:
- scram, for SCRAM-SHA-256 hashing.
- md5, for MD5 hashing.
- plain, for cleartext.
---
doc/src/sgml/libpq.sgml | 6 ++++-
src/bin/psql/command.c | 2 +-
src/bin/scripts/createuser.c | 3 ++-
src/interfaces/libpq/fe-auth.c | 49 +++++++++++++++++++++++++++++++----------
src/interfaces/libpq/libpq-fe.h | 3 ++-
5 files changed, 47 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 4bc5bf3192..fc1aa4b5e5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -5887,7 +5887,8 @@ void PQconninfoFree(PQconninfoOption *connOptions);
<para>
Prepares the encrypted form of a <productname>PostgreSQL</> password.
<synopsis>
-char * PQencryptPassword(const char *passwd, const char *user);
+char * PQencryptPassword(const char *passwd, const char *user,
+ const char *method);
</synopsis>
This function is intended to be used by client applications that
wish to send commands like <literal>ALTER USER joe PASSWORD
@@ -5901,6 +5902,9 @@ char * PQencryptPassword(const char *passwd, const char *user);
memory. The caller can assume the string doesn't contain any
special characters that would require escaping. Use
<function>PQfreemem</> to free the result when done with it.
+ The encryption method of the password can be specified as
+ <literal>md5</> for hashing with MD5, <literal>scram</> for
+ hashing with SCRAM-SHA-256 and <literal>plain</> for cleartext.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 4f4a0aa9bd..25205f589b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1135,7 +1135,7 @@ exec_command(const char *cmd,
else
user = PQuser(pset.db);
- encrypted_password = PQencryptPassword(pw1, user);
+ encrypted_password = PQencryptPassword(pw1, user, "md5");
if (!encrypted_password)
{
diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c
index 3d74797a8f..5af263e34a 100644
--- a/src/bin/scripts/createuser.c
+++ b/src/bin/scripts/createuser.c
@@ -275,7 +275,8 @@ main(int argc, char *argv[])
char *encrypted_password;
encrypted_password = PQencryptPassword(newpassword,
- newuser);
+ newuser,
+ "md5");
if (!encrypted_password)
{
fprintf(stderr, _("Password encryption failed.\n"));
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 5fe7e565a0..c94fb6127f 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -39,6 +39,7 @@
#endif
#include "common/md5.h"
+#include "common/scram-common.h"
#include "libpq-fe.h"
#include "libpq/scram.h"
#include "fe-auth.h"
@@ -919,27 +920,51 @@ pg_fe_getauthname(PQExpBuffer errorMessage)
* be dependent on low-level details like whether the encryption is MD5
* or something else.
*
- * Arguments are the cleartext password, and the SQL name of the user it
- * is for.
+ * Arguments are the cleartext password, the SQL name of the user it
+ * is for, and the name of password hashing method:
+ * - "scram", to hash password using SCRAM-SHA-256.
+ * - "md5", to hash password using MD5.
+ * - "plain", to get a cleartext value of password.
*
- * Return value is a malloc'd string, or NULL if out-of-memory. The client
- * may assume the string doesn't contain any special characters that would
- * require escaping.
+ * Return value is a malloc'd string, or NULL if out-of-memory or in
+ * the event of an error. The client may assume the string doesn't
+ * contain any special characters that would require escaping.
*/
char *
-PQencryptPassword(const char *passwd, const char *user)
+PQencryptPassword(const char *passwd, const char *user, const char *method)
{
char *crypt_pwd;
- crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
- if (!crypt_pwd)
- return NULL;
+ if (strcmp(method, "md5") == 0)
+ {
+ crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
+ if (!crypt_pwd)
+ return NULL;
- if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ {
+ free(crypt_pwd);
+ return NULL;
+ }
+ }
+ else if (strcmp(method, "scram") == 0)
{
- free(crypt_pwd);
- return NULL;
+ crypt_pwd = malloc(SCRAM_VERIFIER_LEN + 1);
+ if (!crypt_pwd)
+ return NULL;
+
+ if (!scram_build_verifier(user, passwd, 0, crypt_pwd))
+ {
+ free(crypt_pwd);
+ return NULL;
+ }
}
+ else if (strcmp(method, "plain") == 0)
+ {
+ crypt_pwd = strdup(passwd);
+ }
+ else
+ return NULL;
return crypt_pwd;
}
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 635af5b50e..c312dd0152 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -596,7 +596,8 @@ extern int PQenv2encoding(void);
/* === in fe-auth.c === */
-extern char *PQencryptPassword(const char *passwd, const char *user);
+extern char *PQencryptPassword(const char *passwd, const char *user,
+ const char *method);
/* === in encnames.c === */
--
2.12.1
0005-Extend-psql-s-password-and-createuser-to-handle-SCRA.patchtext/x-patch; charset=US-ASCII; name=0005-Extend-psql-s-password-and-createuser-to-handle-SCRA.patchDownload
From 0fb421993918b1f1ffddf2b92169f72c27b5f7ec Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Sat, 25 Mar 2017 14:07:16 +0900
Subject: [PATCH 5/5] Extend psql's \password and createuser to handle SCRAM
verifier creation
Depending on the version of PostgreSQL those utilities are connected to,
generate MD5 verifiers when connecting to a server older than 10, and
MD5 otherwise.
---
doc/src/sgml/ref/createuser.sgml | 6 ++++--
doc/src/sgml/ref/psql-ref.sgml | 4 +++-
src/bin/psql/command.c | 9 ++++++++-
src/bin/scripts/createuser.c | 16 +++++++++++++---
4 files changed, 28 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml
index 4332008c68..4454591d79 100644
--- a/doc/src/sgml/ref/createuser.sgml
+++ b/doc/src/sgml/ref/createuser.sgml
@@ -125,7 +125,9 @@ PostgreSQL documentation
<listitem>
<para>
Encrypts the user's password stored in the database. If not
- specified, the default password behavior is used.
+ specified, the default password behavior is used. The password
+ is hashed using SCRAM-SHA-256 when connecting to a version of
+ <productname>PostgreSQL</> newer than 10, and MD5 otherwise.
</para>
</listitem>
</varlistentry>
@@ -477,7 +479,7 @@ PostgreSQL documentation
<prompt>$ </prompt><userinput>createuser -P -s -e joe</userinput>
<computeroutput>Enter password for new role: </computeroutput><userinput>xyzzy</userinput>
<computeroutput>Enter it again: </computeroutput><userinput>xyzzy</userinput>
-<computeroutput>CREATE ROLE joe PASSWORD 'md5b5f5ba1a423792b526f799ae4eb3d59e' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;</computeroutput>
+<computeroutput>CREATE ROLE joe PASSWORD 'scram-sha-256:aPheg4yCAFhStg==:4096:TOHLPF8w+NzqtcxD2oz9w5wPISutHLOXWMKoe4HCvuo=:lTG0OiB/ZH5/hsfUqnndRwfMziY5j5C6FS8IAIwL2nA=' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;</computeroutput>
</screen>
In the above example, the new password isn't actually echoed when typed,
but we show what was typed for clarity. As you see, the password is
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 2a9c412020..c162f59b5a 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2210,7 +2210,9 @@ lo_import 152801
user). This command prompts for the new password, encrypts it, and
sends it to the server as an <command>ALTER ROLE</> command. This
makes sure that the new password does not appear in cleartext in the
- command history, the server log, or elsewhere.
+ command history, the server log, or elsewhere. The password is hashed
+ with SCRAM-SHA-256 when connecting to <productname>PostgreSQL</> 10
+ and newer versions, and with MD5 otherwise.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 25205f589b..b504e4a6cd 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1135,7 +1135,14 @@ exec_command(const char *cmd,
else
user = PQuser(pset.db);
- encrypted_password = PQencryptPassword(pw1, user, "md5");
+ /*
+ * Hash password using SCRAM-SHA-256 when connecting to servers
+ * newer than Postgres 10, and hash with MD5 otherwise.
+ */
+ if (pset.sversion < 100000)
+ encrypted_password = PQencryptPassword(pw1, user, "md5");
+ else
+ encrypted_password = PQencryptPassword(pw1, user, "scram");
if (!encrypted_password)
{
diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c
index 5af263e34a..0107497e23 100644
--- a/src/bin/scripts/createuser.c
+++ b/src/bin/scripts/createuser.c
@@ -274,9 +274,19 @@ main(int argc, char *argv[])
{
char *encrypted_password;
- encrypted_password = PQencryptPassword(newpassword,
- newuser,
- "md5");
+ /*
+ * Hash password using SCRAM-SHA-256 when connecting to servers
+ * newer than Postgres 10, and hash with MD5 otherwise.
+ */
+ if (PQserverVersion(conn) < 100000)
+ encrypted_password = PQencryptPassword(newpassword,
+ newuser,
+ "md5");
+ else
+ encrypted_password = PQencryptPassword(newpassword,
+ newuser,
+ "scram");
+
if (!encrypted_password)
{
fprintf(stderr, _("Password encryption failed.\n"));
--
2.12.1
0002-Refactor-frontend-side-random-number-generation.patchtext/x-patch; charset=US-ASCII; name=0002-Refactor-frontend-side-random-number-generation.patchDownload
From 7451c393920d85dc74858055571aeb8643d9a2a1 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Sat, 25 Mar 2017 13:36:42 +0900
Subject: [PATCH 2/5] Refactor frontend-side random number generation
pg_frontend_random() is moved into its own file in src/common/ to
give other portions of the code the ability to generate random numbers.
This will be used for the SCRAM verifier generation from clients.
---
src/common/Makefile | 3 +-
src/common/frontend_random.c | 86 ++++++++++++++++++++++++++++++++++++
src/include/common/frontend_random.h | 17 +++++++
src/interfaces/libpq/.gitignore | 1 +
src/interfaces/libpq/Makefile | 4 +-
src/interfaces/libpq/fe-auth-scram.c | 59 +------------------------
src/tools/msvc/Mkvcbuild.pm | 2 +-
7 files changed, 110 insertions(+), 62 deletions(-)
create mode 100644 src/common/frontend_random.c
create mode 100644 src/include/common/frontend_random.h
diff --git a/src/common/Makefile b/src/common/Makefile
index 971ddd5ea7..b516ec43f1 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -50,7 +50,8 @@ else
OBJS_COMMON += sha2.o
endif
-OBJS_FRONTEND = $(OBJS_COMMON) fe_memutils.o file_utils.o restricted_token.o
+OBJS_FRONTEND = $(OBJS_COMMON) fe_memutils.o file_utils.o frontend_random.c \
+ restricted_token.o
OBJS_SRV = $(OBJS_COMMON:%.o=%_srv.o)
diff --git a/src/common/frontend_random.c b/src/common/frontend_random.c
new file mode 100644
index 0000000000..7ecc7d5fdf
--- /dev/null
+++ b/src/common/frontend_random.c
@@ -0,0 +1,86 @@
+/*-------------------------------------------------------------------------
+ *
+ * frontend_random.c
+ * Frontend random number generation routine.
+ *
+ * pg_frontend_random() function fills a buffer with random bytes. Normally,
+ * it is just a thin wrapper around pg_strong_random(), but when compiled
+ * with --disable-strong-random, there is a built-in implementation.
+ *
+ * The built-in implementation uses the standard erand48 algorithm, with
+ * a seed calculated using the process ID and a timestamp.
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/common/frontend_random.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#error "This file is not expected to be compiled for backend code"
+#endif
+
+#include "postgres_fe.h"
+#include "common/frontend_random.h"
+
+/* These are needed for getpid(), in the fallback implementation */
+#ifndef HAVE_STRONG_RANDOM
+#include <sys/types.h>
+#include <unistd.h>
+#endif
+
+/*
+ * Random number generator.
+ */
+bool
+pg_frontend_random(char *dst, int len)
+{
+#ifdef HAVE_STRONG_RANDOM
+ return pg_strong_random(dst, len);
+#else
+ int i;
+ char *end = dst + len;
+
+ static unsigned short seed[3];
+ static int mypid = 0;
+
+ pglock_thread();
+
+ if (mypid != getpid())
+ {
+ struct timeval now;
+
+ gettimeofday(&now, NULL);
+
+ seed[0] = now.tv_sec ^ getpid();
+ seed[1] = (unsigned short) (now.tv_usec);
+ seed[2] = (unsigned short) (now.tv_usec >> 16);
+ }
+
+ for (i = 0; dst < end; i++)
+ {
+ uint32 r;
+ int j;
+
+ /*
+ * pg_jrand48 returns a 32-bit integer. Fill the next 4 bytes from
+ * it.
+ */
+ r = (uint32) pg_jrand48(seed);
+
+ for (j = 0; j < 4 && dst < end; j++)
+ {
+ *(dst++) = (char) (r & 0xFF);
+ r >>= 8;
+ }
+ }
+
+ pgunlock_thread();
+
+ return true;
+#endif
+}
diff --git a/src/include/common/frontend_random.h b/src/include/common/frontend_random.h
new file mode 100644
index 0000000000..600c55ace9
--- /dev/null
+++ b/src/include/common/frontend_random.h
@@ -0,0 +1,17 @@
+/*-------------------------------------------------------------------------
+ *
+ * frontend_random.h
+ * Declarations for frontend random number generation
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ *
+ * src/include/common/frontend_random.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef FRONTEND_RANDOM_H
+#define FRONTEND_RANDOM_H
+
+extern bool pg_frontend_random(char *dst, int len);
+
+#endif /* FRONTEND_RANDOM_H */
diff --git a/src/interfaces/libpq/.gitignore b/src/interfaces/libpq/.gitignore
index 2224ada731..f84bc35706 100644
--- a/src/interfaces/libpq/.gitignore
+++ b/src/interfaces/libpq/.gitignore
@@ -2,6 +2,7 @@
/base64.c
/chklocale.c
/crypt.c
+/frontend_random.c
/getaddrinfo.c
/getpeereid.c
/inet_aton.c
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 36b57268a7..269f48ab1e 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -49,7 +49,7 @@ endif
# src/backend/utils/mb
OBJS += encnames.o wchar.o
# src/common
-OBJS += base64.o ip.o md5.o scram-common.o
+OBJS += base64.o frontend_random.o ip.o md5.o scram-common.o
ifeq ($(with_openssl),yes)
OBJS += fe-secure-openssl.o sha2_openssl.o
@@ -106,7 +106,7 @@ backend_src = $(top_srcdir)/src/backend
chklocale.c crypt.c erand48.c getaddrinfo.c getpeereid.c inet_aton.c inet_net_ntop.c noblock.c open.c system.c pgsleep.c pg_strong_random.c pgstrcasecmp.c pqsignal.c snprintf.c strerror.c strlcpy.c thread.c win32error.c win32setlocale.c: % : $(top_srcdir)/src/port/%
rm -f $@ && $(LN_S) $< .
-ip.c md5.c base64.c scram-common.c sha2.c sha2_openssl.c: % : $(top_srcdir)/src/common/%
+ip.c md5.c base64.c frontend_random.c scram-common.c sha2.c sha2_openssl.c: % : $(top_srcdir)/src/common/%
rm -f $@ && $(LN_S) $< .
encnames.c wchar.c: % : $(backend_src)/utils/mb/%
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index a7bb30a141..6390ccf30d 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -15,14 +15,10 @@
#include "postgres_fe.h"
#include "common/base64.h"
+#include "common/frontend_random.h"
#include "common/scram-common.h"
#include "fe-auth.h"
-/* These are needed for getpid(), in the fallback implementation */
-#ifndef HAVE_STRONG_RANDOM
-#include <sys/types.h>
-#include <unistd.h>
-#endif
/*
* Status of exchange messages used for SCRAM authentication via the
@@ -73,7 +69,6 @@ static bool verify_server_proof(fe_scram_state *state);
static void calculate_client_proof(fe_scram_state *state,
const char *client_final_message_without_proof,
uint8 *result);
-static bool pg_frontend_random(char *dst, int len);
/*
* Initialize SCRAM exchange status.
@@ -586,55 +581,3 @@ verify_server_proof(fe_scram_state *state)
return true;
}
-
-/*
- * Random number generator.
- */
-static bool
-pg_frontend_random(char *dst, int len)
-{
-#ifdef HAVE_STRONG_RANDOM
- return pg_strong_random(dst, len);
-#else
- int i;
- char *end = dst + len;
-
- static unsigned short seed[3];
- static int mypid = 0;
-
- pglock_thread();
-
- if (mypid != getpid())
- {
- struct timeval now;
-
- gettimeofday(&now, NULL);
-
- seed[0] = now.tv_sec ^ getpid();
- seed[1] = (unsigned short) (now.tv_usec);
- seed[2] = (unsigned short) (now.tv_usec >> 16);
- }
-
- for (i = 0; dst < end; i++)
- {
- uint32 r;
- int j;
-
- /*
- * pg_jrand48 returns a 32-bit integer. Fill the next 4 bytes from
- * it.
- */
- r = (uint32) pg_jrand48(seed);
-
- for (j = 0; j < 4 && dst < end; j++)
- {
- *(dst++) = (char) (r & 0xFF);
- r >>= 8;
- }
- }
-
- pgunlock_thread();
-
- return true;
-#endif
-}
diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm
index 12f73f344c..615c769483 100644
--- a/src/tools/msvc/Mkvcbuild.pm
+++ b/src/tools/msvc/Mkvcbuild.pm
@@ -125,7 +125,7 @@ sub mkvcbuild
our @pgcommonfrontendfiles = (
@pgcommonallfiles, qw(fe_memutils.c file_utils.c
- restricted_token.c));
+ frontend_random.c restricted_token.c));
our @pgcommonbkndfiles = @pgcommonallfiles;
--
2.12.1
On Sat, Mar 25, 2017 at 1:10 AM, Michael Paquier
<michael.paquier@gmail.com> wrote:
On Fri, Mar 24, 2017 at 10:12 PM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 03/24/2017 03:02 PM, Michael Paquier wrote:
In order to close this thread, I propose to reuse the patches I sent
here to make scram_build_verifier() available to frontends:/messages/by-id/CAB7nPqT4yc3u8wspYkWbG088Ndp6asMH3=Zb___Ck89CTvziYQ@mail.gmail.com
And on top of it modify \password so as it generates a md5 verifier
for pre-9.6 servers and a scram one for post-10 servers by looking at
the backend version of the current connection. What do you think?Yep, sounds like a plan.
And attached is a set of rebased patches, with createuser and psql's
\password extended to do that.
Heikki, are you going to do something about these? We're running out of time.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 04/05/2017 06:53 PM, Robert Haas wrote:
On Sat, Mar 25, 2017 at 1:10 AM, Michael Paquier
<michael.paquier@gmail.com> wrote:On Fri, Mar 24, 2017 at 10:12 PM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 03/24/2017 03:02 PM, Michael Paquier wrote:
In order to close this thread, I propose to reuse the patches I sent
here to make scram_build_verifier() available to frontends:/messages/by-id/CAB7nPqT4yc3u8wspYkWbG088Ndp6asMH3=Zb___Ck89CTvziYQ@mail.gmail.com
And on top of it modify \password so as it generates a md5 verifier
for pre-9.6 servers and a scram one for post-10 servers by looking at
the backend version of the current connection. What do you think?Yep, sounds like a plan.
And attached is a set of rebased patches, with createuser and psql's
\password extended to do that.Heikki, are you going to do something about these? We're running out of time.
Sorry I've been procrastinating. I'm on it now. (We need to do something
about this, feature freeze or not..)
At a quick glance, moving pg_frontend_random() to src/common looks like
a non-starter. It uses pglock_thread() which is internal to libpq, so it
won't compile as it is. I think I'm going to change
scram_build_verifier() to take a pre-generated salt as argument, to
avoid the need for a random number generator in src/common.
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Apr 6, 2017 at 2:11 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
At a quick glance, moving pg_frontend_random() to src/common looks like a
non-starter. It uses pglock_thread() which is internal to libpq, so it won't
compile as it is. I think I'm going to change scram_build_verifier() to take
a pre-generated salt as argument, to avoid the need for a random number
generator in src/common.
Oops. Need an updated set of patches?
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Apr 6, 2017 at 8:04 AM, Michael Paquier
<michael.paquier@gmail.com> wrote:
On Thu, Apr 6, 2017 at 2:11 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
At a quick glance, moving pg_frontend_random() to src/common looks like a
non-starter. It uses pglock_thread() which is internal to libpq, so it won't
compile as it is. I think I'm going to change scram_build_verifier() to take
a pre-generated salt as argument, to avoid the need for a random number
generator in src/common.Oops. Need an updated set of patches?
Attached is an updated set of patches anyway. This is similar to the
last set, except that I removed the part where pg_frontend_random() is
refactored, extending scram_build_verifier() to use a pre-generated
salt.
Hope that helps.
--
Michael
Attachments:
0001-Use-base64-based-encoding-for-stored-and-server-keys.patchapplication/octet-stream; name=0001-Use-base64-based-encoding-for-stored-and-server-keys.patchDownload
From 4b7733cabde0e0a8da4a42d0c6cc736860871f5a Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Sat, 25 Mar 2017 13:31:38 +0900
Subject: [PATCH 1/5] Use base64-based encoding for stored and server keys in
SCRAM verifiers
In order to be able to generate a SCRAM verifier even for frontends, let's
simplify the tools used to generate it and switch all the elements of the
verifiers to be base64-encoded using the routines already in place in
src/common/.
---
doc/src/sgml/catalogs.sgml | 2 +-
src/backend/libpq/auth-scram.c | 30 +++++++++++++++++-------------
src/test/regress/expected/password.out | 8 ++++----
src/test/regress/sql/password.sql | 8 ++++----
4 files changed, 26 insertions(+), 22 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 5883673448..3d5999d829 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1382,7 +1382,7 @@
identify the password as a SCRAM-SHA-256 verifier. The second field is a
salt, Base64-encoded, and the third field is the number of iterations used
to generate the password. The fourth field and fifth field are the stored
- key and server key, respectively, in hexadecimal format. A password that
+ key and server key, respectively, in Base64 format. A password that
does not follow either of those formats is assumed to be unencrypted.
</para>
</sect1>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 14ddc8bd54..8fef0e995d 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -339,8 +339,8 @@ scram_build_verifier(const char *username, const char *password,
int iterations)
{
uint8 keybuf[SCRAM_KEY_LEN + 1];
- char storedkey_hex[SCRAM_KEY_LEN * 2 + 1];
- char serverkey_hex[SCRAM_KEY_LEN * 2 + 1];
+ char *encoded_storedkey;
+ char *encoded_serverkey;
char salt[SCRAM_SALT_LEN];
char *encoded_salt;
int encoded_len;
@@ -360,20 +360,25 @@ scram_build_verifier(const char *username, const char *password,
encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt);
encoded_salt[encoded_len] = '\0';
- /* Calculate StoredKey, and encode it in hex */
+ /* Calculate StoredKey, and encode it in base64 */
+ encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
- (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, storedkey_hex);
- storedkey_hex[SCRAM_KEY_LEN * 2] = '\0';
+ encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
+ encoded_storedkey);
+ encoded_storedkey[encoded_len] = '\0';
/* And same for ServerKey */
+ encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
SCRAM_SERVER_KEY_NAME, keybuf);
- (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, serverkey_hex);
- serverkey_hex[SCRAM_KEY_LEN * 2] = '\0';
+ encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
+ encoded_serverkey);
+ encoded_serverkey[encoded_len] = '\0';
- return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations, storedkey_hex, serverkey_hex);
+ return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations,
+ encoded_storedkey, encoded_serverkey);
}
/*
@@ -483,17 +488,16 @@ parse_scram_verifier(const char *verifier, char **salt, int *iterations,
/* storedkey */
if ((p = strtok(NULL, ":")) == NULL)
goto invalid_verifier;
- if (strlen(p) != SCRAM_KEY_LEN * 2)
+ if (strlen(p) != pg_b64_enc_len(SCRAM_KEY_LEN) - 1)
goto invalid_verifier;
-
- hex_decode(p, SCRAM_KEY_LEN * 2, (char *) stored_key);
+ pg_b64_decode(p, pg_b64_enc_len(SCRAM_KEY_LEN), (char *) stored_key);
/* serverkey */
if ((p = strtok(NULL, ":")) == NULL)
goto invalid_verifier;
- if (strlen(p) != SCRAM_KEY_LEN * 2)
+ if (strlen(p) != pg_b64_enc_len(SCRAM_KEY_LEN) - 1)
goto invalid_verifier;
- hex_decode(p, SCRAM_KEY_LEN * 2, (char *) server_key);
+ pg_b64_decode(p, pg_b64_enc_len(SCRAM_KEY_LEN), (char *) server_key);
pfree(v);
return true;
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index c503e43abe..0cdb6141e2 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -23,11 +23,11 @@ CREATE ROLE regress_passwd5 PASSWORD NULL;
-- check list of created entries
--
-- The scram verifier will look something like:
--- scram-sha-256:E4HxLGtnRzsYwg==:4096:5ebc825510cb7862efd87dfa638d8337179e6913a724441dc9e888a856fbc10c:e966b1c72fad89d69aaebb156eae04edc9581286f92207c044711e79cd461bee
+-- scram-sha-256:E4HxLGtnRzsYwg==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo=
--
-- Since the salt is random, the exact value stored will be different on every test
-- run. Use a regular expression to mask the changing parts.
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
@@ -59,11 +59,11 @@ ALTER ROLE regress_passwd1 UNENCRYPTED PASSWORD 'foo'; -- unencrypted
ALTER ROLE regress_passwd2 UNENCRYPTED PASSWORD 'md5dfa155cadd5f4ad57860162f3fab9cdb'; -- encrypted with MD5
SET password_encryption = 'md5';
ALTER ROLE regress_passwd3 ENCRYPTED PASSWORD 'foo'; -- encrypted with MD5
-ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:3ded2376f7aafa93b1bdbd71bcc18b7d6ee50ed018029cc583d152ef3fc7d430:a6dd36dfc94c181956a6ae95f05e01b1864f0a22a2657d1de4ba84d2a24dc438'; -- client-supplied SCRAM verifier, use as it is
+ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; -- client-supplied SCRAM verifier, use as it is
SET password_encryption = 'scram';
ALTER ROLE regress_passwd5 ENCRYPTED PASSWORD 'foo'; -- create SCRAM verifier
CREATE ROLE regress_passwd6 ENCRYPTED PASSWORD 'md53725413363ab045e20521bf36b8d8d7f'; -- encrypted with MD5, use as it is
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql
index f4b3a9ac3a..df1ff989cc 100644
--- a/src/test/regress/sql/password.sql
+++ b/src/test/regress/sql/password.sql
@@ -24,11 +24,11 @@ CREATE ROLE regress_passwd5 PASSWORD NULL;
-- check list of created entries
--
-- The scram verifier will look something like:
--- scram-sha-256:E4HxLGtnRzsYwg==:4096:5ebc825510cb7862efd87dfa638d8337179e6913a724441dc9e888a856fbc10c:e966b1c72fad89d69aaebb156eae04edc9581286f92207c044711e79cd461bee
+-- scram-sha-256:E4HxLGtnRzsYwg==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo=
--
-- Since the salt is random, the exact value stored will be different on every test
-- run. Use a regular expression to mask the changing parts.
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
@@ -48,13 +48,13 @@ ALTER ROLE regress_passwd2 UNENCRYPTED PASSWORD 'md5dfa155cadd5f4ad57860162f3fab
SET password_encryption = 'md5';
ALTER ROLE regress_passwd3 ENCRYPTED PASSWORD 'foo'; -- encrypted with MD5
-ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:3ded2376f7aafa93b1bdbd71bcc18b7d6ee50ed018029cc583d152ef3fc7d430:a6dd36dfc94c181956a6ae95f05e01b1864f0a22a2657d1de4ba84d2a24dc438'; -- client-supplied SCRAM verifier, use as it is
+ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; -- client-supplied SCRAM verifier, use as it is
SET password_encryption = 'scram';
ALTER ROLE regress_passwd5 ENCRYPTED PASSWORD 'foo'; -- create SCRAM verifier
CREATE ROLE regress_passwd6 ENCRYPTED PASSWORD 'md53725413363ab045e20521bf36b8d8d7f'; -- encrypted with MD5, use as it is
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
--
2.12.2
0002-Move-routine-to-build-SCRAM-verifier-into-src-common.patchapplication/octet-stream; name=0002-Move-routine-to-build-SCRAM-verifier-into-src-common.patchDownload
From 62235ef08fa8ce64f90e55e640df39c5b9d23f10 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 7 Apr 2017 10:10:15 +0900
Subject: [PATCH 2/5] Move routine to build SCRAM verifier into src/common/
This is aimed at being used by libpq to allow frontend-side creation
of SCRAM verifiers. The result is not anymore allocated by the routine
itself, the caller is responsible for that, similarly to md5.
---
src/backend/libpq/auth-scram.c | 65 +++++++--------------------------------
src/backend/libpq/crypt.c | 21 +++++++++++--
src/common/scram-common.c | 56 +++++++++++++++++++++++++++++++++
src/include/common/scram-common.h | 20 ++++++++++++
src/include/libpq/scram.h | 5 +--
5 files changed, 107 insertions(+), 60 deletions(-)
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 8fef0e995d..22f1e1a6c6 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -172,9 +172,18 @@ pg_be_scram_init(const char *username, const char *shadow_pass)
* The stored password is in plain format. Generate a fresh SCRAM
* verifier from it, and proceed with that.
*/
- char *verifier;
+ char *verifier = palloc(SCRAM_VERIFIER_LEN + 1);
+ char salt[SCRAM_SALT_LEN];
- verifier = scram_build_verifier(username, shadow_pass, 0);
+ if (!pg_backend_random(salt, SCRAM_SALT_LEN))
+ {
+ ereport(LOG,
+ (errcode(ERRCODE_INTERNAL_ERROR),
+ errmsg("could not generate random salt")));
+ return NULL;
+ }
+
+ (void) scram_build_verifier(username, shadow_pass, salt, 0, verifier);
(void) parse_scram_verifier(verifier, &state->salt, &state->iterations,
state->StoredKey, state->ServerKey);
@@ -328,58 +337,6 @@ pg_be_scram_exchange(void *opaq, char *input, int inputlen,
return result;
}
-/*
- * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
- *
- * If iterations is 0, default number of iterations is used. The result is
- * palloc'd, so caller is responsible for freeing it.
- */
-char *
-scram_build_verifier(const char *username, const char *password,
- int iterations)
-{
- uint8 keybuf[SCRAM_KEY_LEN + 1];
- char *encoded_storedkey;
- char *encoded_serverkey;
- char salt[SCRAM_SALT_LEN];
- char *encoded_salt;
- int encoded_len;
-
- if (iterations <= 0)
- iterations = SCRAM_ITERATIONS_DEFAULT;
-
- if (!pg_backend_random(salt, SCRAM_SALT_LEN))
- {
- ereport(LOG,
- (errcode(ERRCODE_INTERNAL_ERROR),
- errmsg("could not generate random salt")));
- return NULL;
- }
-
- encoded_salt = palloc(pg_b64_enc_len(SCRAM_SALT_LEN) + 1);
- encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt);
- encoded_salt[encoded_len] = '\0';
-
- /* Calculate StoredKey, and encode it in base64 */
- encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
- scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
- iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
- scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
- encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
- encoded_storedkey);
- encoded_storedkey[encoded_len] = '\0';
-
- /* And same for ServerKey */
- encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
- scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
- SCRAM_SERVER_KEY_NAME, keybuf);
- encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
- encoded_serverkey);
- encoded_serverkey[encoded_len] = '\0';
-
- return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations,
- encoded_storedkey, encoded_serverkey);
-}
/*
* Verify a plaintext password against a SCRAM verifier. This is used when
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 34beab5334..a0426f5450 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,9 +20,11 @@
#include "catalog/pg_authid.h"
#include "common/md5.h"
+#include "common/scram-common.h"
#include "libpq/crypt.h"
#include "libpq/scram.h"
#include "miscadmin.h"
+#include "utils/backend_random.h"
#include "utils/builtins.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
@@ -156,8 +158,23 @@ encrypt_password(PasswordType target_type, const char *role,
switch (guessed_type)
{
case PASSWORD_TYPE_PLAINTEXT:
- return scram_build_verifier(role, password, 0);
-
+ {
+ char salt[SCRAM_SALT_LEN];
+
+ if (!pg_backend_random(salt, SCRAM_SALT_LEN))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INTERNAL_ERROR),
+ errmsg("could not generate random salt")));
+ return NULL;
+ }
+
+ encrypted_password = palloc(SCRAM_VERIFIER_LEN + 1);
+ if (!scram_build_verifier(role, password, salt, 0,
+ encrypted_password))
+ elog(ERROR, "password encryption failed");
+ return encrypted_password;
+ }
case PASSWORD_TYPE_MD5:
/*
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index e44f38f652..03bdc7b9d2 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -184,3 +184,59 @@ scram_ClientOrServerKey(const char *password,
scram_HMAC_update(&ctx, keystr, strlen(keystr));
scram_HMAC_final(result, &ctx);
}
+
+/*
+ * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
+ *
+ * If iterations is 0, default number of iterations is used. The result is
+ * stored in "verifier" that caller is responsible to allocate a buffer of
+ * size SCRAM_VERIFIER_LEN. Returns true if the verifier has been generated,
+ * false otherwise. It is important for this routine to do no memory
+ * allocations. The salt defined by the caller needs to be a buffer pre-filled
+ * with random data of length SCRAM_SALT_LEN.
+ */
+bool
+scram_build_verifier(const char *username, const char *password,
+ const char *salt, int iterations, char *verifier)
+{
+ uint8 keybuf[SCRAM_KEY_LEN + 1];
+ char intbuf[12];
+ char *p;
+
+ if (iterations <= 0)
+ iterations = SCRAM_ITERATIONS_DEFAULT;
+
+ /* Fill in the data of the verifier */
+ p = verifier;
+ memcpy(p, SCRAM_VERIFIER_PREFIX, strlen(SCRAM_VERIFIER_PREFIX));
+ p += strlen(SCRAM_VERIFIER_PREFIX);
+ *p++ = ':';
+
+ /* salt */
+ (void) pg_b64_encode(salt, SCRAM_SALT_LEN, p);
+ p += pg_b64_enc_len(SCRAM_SALT_LEN);
+ *p++ = ':';
+
+ /* iterations */
+ sprintf(intbuf, "%d", iterations);
+ memcpy(p, intbuf, strlen(intbuf));
+ p += strlen(intbuf);
+ *p++ = ':';
+
+ /* Calculate StoredKey, and encode it in base64 */
+ scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
+ iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
+ scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
+ (void) pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, p);
+ p += pg_b64_enc_len(SCRAM_KEY_LEN) - 1;
+ *p++ = ':';
+
+ /* And same for ServerKey */
+ scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
+ SCRAM_SERVER_KEY_NAME, keybuf);
+ (void) pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, p);
+ p += pg_b64_enc_len(SCRAM_KEY_LEN) - 1;
+ *p++ = '\0';
+
+ return true;
+}
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index 6740069eee..fcf3b98ba6 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -13,6 +13,7 @@
#ifndef SCRAM_COMMON_H
#define SCRAM_COMMON_H
+#include "common/base64.h"
#include "common/sha2.h"
/* Length of SCRAM keys (client and server) */
@@ -38,6 +39,22 @@
#define SCRAM_SERVER_KEY_NAME "Server Key"
#define SCRAM_CLIENT_KEY_NAME "Client Key"
+#define SCRAM_VERIFIER_PREFIX "scram-sha-256"
+
+/*
+ * Length of a SCRAM verifier, which is made of the following five fields
+ * separated by a colon:
+ * - prefix "scram-sha-256", made of 13 characters.
+ * - 4 colon separators.
+ * - 32-bit number of interations, up to 11 characters.
+ * - base64-encoded salt of length SCRAM_SALT_LEN
+ * - base64-encoded stored key of length SCRAM_KEY_LEN
+ * - base64-encoded server key of length SCRAM_KEY_LEN
+ */
+#define SCRAM_VERIFIER_LEN (strlen("scram-sha-256") + 4 + 11 + \
+ pg_b64_enc_len(SCRAM_SALT_LEN) + \
+ pg_b64_enc_len(SCRAM_KEY_LEN) * 2)
+
/*
* Context data for HMAC used in SCRAM authentication.
*/
@@ -55,5 +72,8 @@ extern void scram_H(const uint8 *str, int len, uint8 *result);
extern void scram_ClientOrServerKey(const char *password, const char *salt,
int saltlen, int iterations,
const char *keystr, uint8 *result);
+extern bool scram_build_verifier(const char *username, const char *password,
+ const char *salt, int iterations,
+ char *verifier);
#endif /* SCRAM_COMMON_H */
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index e373f0c07e..803fc4ef7a 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -26,10 +26,7 @@ extern void *pg_be_scram_init(const char *username, const char *shadow_pass);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
-/* Routines to handle and check SCRAM-SHA-256 verifier */
-extern char *scram_build_verifier(const char *username,
- const char *password,
- int iterations);
+/* Routines to check SCRAM-SHA-256 verifier */
extern bool is_scram_verifier(const char *verifier);
extern bool scram_verify_plain_password(const char *username,
const char *password, const char *verifier);
--
2.12.2
0003-Refactor-frontend-side-random-number-generation.patchapplication/octet-stream; name=0003-Refactor-frontend-side-random-number-generation.patchDownload
From 6f8baee4ef5fbc0b7d4b2a352b7667f70d1e0c31 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 7 Apr 2017 10:24:44 +0900
Subject: [PATCH 3/5] Refactor frontend-side random number generation
pg_frontend_random() is moved into its own file in libpq/ to give other
portions of the libpq code the ability to generate random numbers. This
will be used for the SCRAM verifier generation from clients.
---
src/interfaces/libpq/Makefile | 2 +-
src/interfaces/libpq/fe-auth-scram.c | 53 -----------------------
src/interfaces/libpq/libpq-int.h | 1 +
src/interfaces/libpq/libpq-random.c | 84 ++++++++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-random.h | 17 ++++++++
src/tools/msvc/Install.pm | 2 +-
6 files changed, 104 insertions(+), 55 deletions(-)
create mode 100644 src/interfaces/libpq/libpq-random.c
create mode 100644 src/interfaces/libpq/libpq-random.h
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 36b57268a7..1d20ae9ac2 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -33,7 +33,7 @@ LIBS := $(LIBS:-lpgport=)
# OBJS from this file.
OBJS= fe-auth.o fe-auth-scram.o fe-connect.o fe-exec.o fe-misc.o fe-print.o fe-lobj.o \
fe-protocol2.o fe-protocol3.o pqexpbuffer.o fe-secure.o \
- libpq-events.o
+ libpq-events.o libpq-random.o
# libpgport C files we always use
OBJS += chklocale.o inet_net_ntop.o noblock.o pgstrcasecmp.o pqsignal.o \
thread.o
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 818ade4993..e6661a6524 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -73,7 +73,6 @@ static bool verify_server_proof(fe_scram_state *state);
static void calculate_client_proof(fe_scram_state *state,
const char *client_final_message_without_proof,
uint8 *result);
-static bool pg_frontend_random(char *dst, int len);
/*
* Initialize SCRAM exchange status.
@@ -586,55 +585,3 @@ verify_server_proof(fe_scram_state *state)
return true;
}
-
-/*
- * Random number generator.
- */
-static bool
-pg_frontend_random(char *dst, int len)
-{
-#ifdef HAVE_STRONG_RANDOM
- return pg_strong_random(dst, len);
-#else
- int i;
- char *end = dst + len;
-
- static unsigned short seed[3];
- static int mypid = 0;
-
- pglock_thread();
-
- if (mypid != getpid())
- {
- struct timeval now;
-
- gettimeofday(&now, NULL);
-
- seed[0] = now.tv_sec ^ getpid();
- seed[1] = (unsigned short) (now.tv_usec);
- seed[2] = (unsigned short) (now.tv_usec >> 16);
- }
-
- for (i = 0; dst < end; i++)
- {
- uint32 r;
- int j;
-
- /*
- * pg_jrand48 returns a 32-bit integer. Fill the next 4 bytes from
- * it.
- */
- r = (uint32) pg_jrand48(seed);
-
- for (j = 0; j < 4 && dst < end; j++)
- {
- *(dst++) = (char) (r & 0xFF);
- r >>= 8;
- }
- }
-
- pgunlock_thread();
-
- return true;
-#endif
-}
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index b8ec3418c5..6b72a17a75 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -22,6 +22,7 @@
/* We assume libpq-fe.h has already been included. */
#include "libpq-events.h"
+#include "libpq-random.h"
#include <time.h>
#ifndef WIN32
diff --git a/src/interfaces/libpq/libpq-random.c b/src/interfaces/libpq/libpq-random.c
new file mode 100644
index 0000000000..5e788dc821
--- /dev/null
+++ b/src/interfaces/libpq/libpq-random.c
@@ -0,0 +1,84 @@
+/*-------------------------------------------------------------------------
+ *
+ * libpq-random.c
+ * Frontend random number generation routine for libpq.
+ *
+ * pg_frontend_random() function fills a buffer with random bytes. Normally,
+ * it is just a thin wrapper around pg_strong_random(), but when compiled
+ * with --disable-strong-random, there is a built-in implementation.
+ *
+ * The built-in implementation uses the standard erand48 algorithm, with
+ * a seed calculated using the process ID and a timestamp.
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/interfaces/libpq/libpq-random.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+/* These are needed for getpid(), in the fallback implementation */
+#ifndef HAVE_STRONG_RANDOM
+#include <sys/types.h>
+#include <unistd.h>
+#endif
+
+/*
+ * Random number generator.
+ */
+bool
+pg_frontend_random(char *dst, int len)
+{
+#ifdef HAVE_STRONG_RANDOM
+ return pg_strong_random(dst, len);
+#else
+ int i;
+ char *end = dst + len;
+
+ static unsigned short seed[3];
+ static int mypid = 0;
+
+ pglock_thread();
+
+ if (mypid != getpid())
+ {
+ struct timeval now;
+
+ gettimeofday(&now, NULL);
+
+ seed[0] = now.tv_sec ^ getpid();
+ seed[1] = (unsigned short) (now.tv_usec);
+ seed[2] = (unsigned short) (now.tv_usec >> 16);
+ }
+
+ for (i = 0; dst < end; i++)
+ {
+ uint32 r;
+ int j;
+
+ /*
+ * pg_jrand48 returns a 32-bit integer. Fill the next 4 bytes from
+ * it.
+ */
+ r = (uint32) pg_jrand48(seed);
+
+ for (j = 0; j < 4 && dst < end; j++)
+ {
+ *(dst++) = (char) (r & 0xFF);
+ r >>= 8;
+ }
+ }
+
+ pgunlock_thread();
+
+ return true;
+#endif
+}
diff --git a/src/interfaces/libpq/libpq-random.h b/src/interfaces/libpq/libpq-random.h
new file mode 100644
index 0000000000..054998b8d9
--- /dev/null
+++ b/src/interfaces/libpq/libpq-random.h
@@ -0,0 +1,17 @@
+/*-------------------------------------------------------------------------
+ *
+ * frontend_random.h
+ * Declarations for frontend random number generation
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ *
+ * src/interfaces/libpq/libpq-random.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef LIBPQ_RANDOM_H
+#define LIBPQ_RANDOM_H
+
+extern bool pg_frontend_random(char *dst, int len);
+
+#endif /* LIBPQ_RANDOM_H */
diff --git a/src/tools/msvc/Install.pm b/src/tools/msvc/Install.pm
index 35ad5b8a44..4acd6592ad 100644
--- a/src/tools/msvc/Install.pm
+++ b/src/tools/msvc/Install.pm
@@ -601,7 +601,7 @@ sub CopyIncludeFiles
CopyFiles(
'Libpq internal headers',
$target . '/include/internal/',
- 'src/interfaces/libpq/', 'libpq-int.h', 'pqexpbuffer.h');
+ 'src/interfaces/libpq/', 'libpq-int.h', 'libpq-random.h', 'pqexpbuffer.h');
CopyFiles(
'Internal headers',
--
2.12.2
0004-Extend-PQencryptPassword-with-a-hashing-method.patchapplication/octet-stream; name=0004-Extend-PQencryptPassword-with-a-hashing-method.patchDownload
From 2c06cfea054dab44b8df393700e052ff01e3bd22 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 7 Apr 2017 10:39:27 +0900
Subject: [PATCH 4/5] Extend PQencryptPassword with a hashing method
This extra argument can use the following values when hashing the
password:
- scram, for SCRAM-SHA-256 hashing.
- md5, for MD5 hashing.
- plain, for cleartext.
---
doc/src/sgml/libpq.sgml | 6 ++++-
src/bin/psql/command.c | 2 +-
src/bin/scripts/createuser.c | 3 ++-
src/interfaces/libpq/fe-auth.c | 54 ++++++++++++++++++++++++++++++++---------
src/interfaces/libpq/libpq-fe.h | 3 ++-
5 files changed, 52 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 4bc5bf3192..fc1aa4b5e5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -5887,7 +5887,8 @@ void PQconninfoFree(PQconninfoOption *connOptions);
<para>
Prepares the encrypted form of a <productname>PostgreSQL</> password.
<synopsis>
-char * PQencryptPassword(const char *passwd, const char *user);
+char * PQencryptPassword(const char *passwd, const char *user,
+ const char *method);
</synopsis>
This function is intended to be used by client applications that
wish to send commands like <literal>ALTER USER joe PASSWORD
@@ -5901,6 +5902,9 @@ char * PQencryptPassword(const char *passwd, const char *user);
memory. The caller can assume the string doesn't contain any
special characters that would require escaping. Use
<function>PQfreemem</> to free the result when done with it.
+ The encryption method of the password can be specified as
+ <literal>md5</> for hashing with MD5, <literal>scram</> for
+ hashing with SCRAM-SHA-256 and <literal>plain</> for cleartext.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 494f468575..9716c98fc2 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1882,7 +1882,7 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
else
user = PQuser(pset.db);
- encrypted_password = PQencryptPassword(pw1, user);
+ encrypted_password = PQencryptPassword(pw1, user, "md5");
if (!encrypted_password)
{
diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c
index 3d74797a8f..5af263e34a 100644
--- a/src/bin/scripts/createuser.c
+++ b/src/bin/scripts/createuser.c
@@ -275,7 +275,8 @@ main(int argc, char *argv[])
char *encrypted_password;
encrypted_password = PQencryptPassword(newpassword,
- newuser);
+ newuser,
+ "md5");
if (!encrypted_password)
{
fprintf(stderr, _("Password encryption failed.\n"));
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 5fe7e565a0..40d4be3ca6 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -39,6 +39,7 @@
#endif
#include "common/md5.h"
+#include "common/scram-common.h"
#include "libpq-fe.h"
#include "libpq/scram.h"
#include "fe-auth.h"
@@ -919,27 +920,56 @@ pg_fe_getauthname(PQExpBuffer errorMessage)
* be dependent on low-level details like whether the encryption is MD5
* or something else.
*
- * Arguments are the cleartext password, and the SQL name of the user it
- * is for.
+ * Arguments are the cleartext password, the SQL name of the user it
+ * is for, and the name of password hashing method:
+ * - "scram", to hash password using SCRAM-SHA-256.
+ * - "md5", to hash password using MD5.
+ * - "plain", to get a cleartext value of password.
*
- * Return value is a malloc'd string, or NULL if out-of-memory. The client
- * may assume the string doesn't contain any special characters that would
- * require escaping.
+ * Return value is a malloc'd string, or NULL if out-of-memory or in
+ * the event of an error. The client may assume the string doesn't
+ * contain any special characters that would require escaping.
*/
char *
-PQencryptPassword(const char *passwd, const char *user)
+PQencryptPassword(const char *passwd, const char *user, const char *method)
{
char *crypt_pwd;
- crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
- if (!crypt_pwd)
- return NULL;
+ if (strcmp(method, "md5") == 0)
+ {
+ crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
+ if (!crypt_pwd)
+ return NULL;
- if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ {
+ free(crypt_pwd);
+ return NULL;
+ }
+ }
+ else if (strcmp(method, "scram") == 0)
{
- free(crypt_pwd);
- return NULL;
+ char salt[SCRAM_SALT_LEN];
+
+ crypt_pwd = malloc(SCRAM_VERIFIER_LEN + 1);
+ if (!crypt_pwd)
+ return NULL;
+
+ if (!pg_frontend_random(salt, SCRAM_SALT_LEN))
+ return NULL;
+
+ if (!scram_build_verifier(user, passwd, salt, 0, crypt_pwd))
+ {
+ free(crypt_pwd);
+ return NULL;
+ }
}
+ else if (strcmp(method, "plain") == 0)
+ {
+ crypt_pwd = strdup(passwd);
+ }
+ else
+ return NULL;
return crypt_pwd;
}
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 635af5b50e..c312dd0152 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -596,7 +596,8 @@ extern int PQenv2encoding(void);
/* === in fe-auth.c === */
-extern char *PQencryptPassword(const char *passwd, const char *user);
+extern char *PQencryptPassword(const char *passwd, const char *user,
+ const char *method);
/* === in encnames.c === */
--
2.12.2
0005-Extend-psql-s-password-and-createuser-to-handle-SCRA.patchapplication/octet-stream; name=0005-Extend-psql-s-password-and-createuser-to-handle-SCRA.patchDownload
From 78c9d261c5ea611dd19c97053201e2fea0c54cb5 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Sat, 25 Mar 2017 14:07:16 +0900
Subject: [PATCH 5/5] Extend psql's \password and createuser to handle SCRAM
verifier creation
Depending on the version of PostgreSQL those utilities are connected to,
generate MD5 verifiers when connecting to a server older than 10, and
MD5 otherwise.
---
doc/src/sgml/ref/createuser.sgml | 6 ++++--
doc/src/sgml/ref/psql-ref.sgml | 4 +++-
src/bin/psql/command.c | 9 ++++++++-
src/bin/scripts/createuser.c | 16 +++++++++++++---
4 files changed, 28 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml
index 4332008c68..4454591d79 100644
--- a/doc/src/sgml/ref/createuser.sgml
+++ b/doc/src/sgml/ref/createuser.sgml
@@ -125,7 +125,9 @@ PostgreSQL documentation
<listitem>
<para>
Encrypts the user's password stored in the database. If not
- specified, the default password behavior is used.
+ specified, the default password behavior is used. The password
+ is hashed using SCRAM-SHA-256 when connecting to a version of
+ <productname>PostgreSQL</> newer than 10, and MD5 otherwise.
</para>
</listitem>
</varlistentry>
@@ -477,7 +479,7 @@ PostgreSQL documentation
<prompt>$ </prompt><userinput>createuser -P -s -e joe</userinput>
<computeroutput>Enter password for new role: </computeroutput><userinput>xyzzy</userinput>
<computeroutput>Enter it again: </computeroutput><userinput>xyzzy</userinput>
-<computeroutput>CREATE ROLE joe PASSWORD 'md5b5f5ba1a423792b526f799ae4eb3d59e' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;</computeroutput>
+<computeroutput>CREATE ROLE joe PASSWORD 'scram-sha-256:aPheg4yCAFhStg==:4096:TOHLPF8w+NzqtcxD2oz9w5wPISutHLOXWMKoe4HCvuo=:lTG0OiB/ZH5/hsfUqnndRwfMziY5j5C6FS8IAIwL2nA=' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;</computeroutput>
</screen>
In the above example, the new password isn't actually echoed when typed,
but we show what was typed for clarity. As you see, the password is
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 3b86612862..749221aa76 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2380,7 +2380,9 @@ lo_import 152801
user). This command prompts for the new password, encrypts it, and
sends it to the server as an <command>ALTER ROLE</> command. This
makes sure that the new password does not appear in cleartext in the
- command history, the server log, or elsewhere.
+ command history, the server log, or elsewhere. The password is hashed
+ with SCRAM-SHA-256 when connecting to <productname>PostgreSQL</> 10
+ and newer versions, and with MD5 otherwise.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 9716c98fc2..4e9b526e43 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1882,7 +1882,14 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
else
user = PQuser(pset.db);
- encrypted_password = PQencryptPassword(pw1, user, "md5");
+ /*
+ * Hash password using SCRAM-SHA-256 when connecting to servers
+ * newer than Postgres 10, and hash with MD5 otherwise.
+ */
+ if (pset.sversion < 100000)
+ encrypted_password = PQencryptPassword(pw1, user, "md5");
+ else
+ encrypted_password = PQencryptPassword(pw1, user, "scram");
if (!encrypted_password)
{
diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c
index 5af263e34a..0107497e23 100644
--- a/src/bin/scripts/createuser.c
+++ b/src/bin/scripts/createuser.c
@@ -274,9 +274,19 @@ main(int argc, char *argv[])
{
char *encrypted_password;
- encrypted_password = PQencryptPassword(newpassword,
- newuser,
- "md5");
+ /*
+ * Hash password using SCRAM-SHA-256 when connecting to servers
+ * newer than Postgres 10, and hash with MD5 otherwise.
+ */
+ if (PQserverVersion(conn) < 100000)
+ encrypted_password = PQencryptPassword(newpassword,
+ newuser,
+ "md5");
+ else
+ encrypted_password = PQencryptPassword(newpassword,
+ newuser,
+ "scram");
+
if (!encrypted_password)
{
fprintf(stderr, _("Password encryption failed.\n"));
--
2.12.2
On Wed, Apr 05, 2017 at 08:11:25PM +0300, Heikki Linnakangas wrote:
On 04/05/2017 06:53 PM, Robert Haas wrote:
On Sat, Mar 25, 2017 at 1:10 AM, Michael Paquier
<michael.paquier@gmail.com> wrote:On Fri, Mar 24, 2017 at 10:12 PM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 03/24/2017 03:02 PM, Michael Paquier wrote:
In order to close this thread, I propose to reuse the patches I sent
here to make scram_build_verifier() available to frontends:/messages/by-id/CAB7nPqT4yc3u8wspYkWbG088Ndp6asMH3=Zb___Ck89CTvziYQ@mail.gmail.com
And on top of it modify \password so as it generates a md5 verifier
for pre-9.6 servers and a scram one for post-10 servers by looking at
the backend version of the current connection. What do you think?Yep, sounds like a plan.
And attached is a set of rebased patches, with createuser and psql's
\password extended to do that.Heikki, are you going to do something about these? We're running out of time.
Sorry I've been procrastinating. I'm on it now. (We need to do something
about this, feature freeze or not..)
[Action required within three days. This is a generic notification.]
The above-described topic is currently a PostgreSQL 10 open item. Heikki,
since you committed the patch believed to have created it, you own this open
item. If some other commit is more relevant or if this does not belong as a
v10 open item, please let us know. Otherwise, please observe the policy on
open item ownership[1]/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com and send a status update within three calendar days of
this message. Include a date for your subsequent status update. Testers may
discover new open items at any time, and I want to plan to get them all fixed
well in advance of shipping v10. Consequently, I will appreciate your efforts
toward speedy resolution. Thanks.
[1]: /messages/by-id/20170404140717.GA2675809@tornado.leadboat.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Apr 10, 2017 at 12:53 PM, Noah Misch <noah@leadboat.com> wrote:
On Wed, Apr 05, 2017 at 08:11:25PM +0300, Heikki Linnakangas wrote:
Heikki, are you going to do something about these? We're running out of time.
Sorry I've been procrastinating. I'm on it now. (We need to do something
about this, feature freeze or not..)
As there have been some conflicts because of the commit of SASLprep,
here is a rebased set of patches. A couple of things worth noting:
- SASLprep does an allocation of the prepared password string. It is
definitely better to do all the ground work in pg_saslprep but this
costs a free() call with a #ifdef FRONTEND at the end of
scram_build_verifier().
- Patch 0005 does that:
+ /*
+ * Hash password using SCRAM-SHA-256 when connecting to servers
+ * newer than Postgres 10, and hash with MD5 otherwise.
+ */
+ if (pset.sversion < 100000)
+ encrypted_password = PQencryptPassword(pw1, user, "md5");
+ else
+ encrypted_password = PQencryptPassword(pw1, user, "scram");
Actually I am thinking that guessing the hashing function according to
the value of password_encryption would make the most sense. Thoughts?
--
Michael
VMware vCenter server
www.vmware.com
Attachments:
0001-Use-base64-based-encoding-for-stored-and-server-keys.patchapplication/octet-stream; name=0001-Use-base64-based-encoding-for-stored-and-server-keys.patchDownload
From 6f66728f4173b8bfb6850ae936f60e81eae1657c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Sat, 25 Mar 2017 13:31:38 +0900
Subject: [PATCH 1/5] Use base64-based encoding for stored and server keys in
SCRAM verifiers
In order to be able to generate a SCRAM verifier even for frontends, let's
simplify the tools used to generate it and switch all the elements of the
verifiers to be base64-encoded using the routines already in place in
src/common/.
---
doc/src/sgml/catalogs.sgml | 2 +-
src/backend/libpq/auth-scram.c | 30 +++++++++++++++++-------------
src/test/regress/expected/password.out | 8 ++++----
src/test/regress/sql/password.sql | 8 ++++----
4 files changed, 26 insertions(+), 22 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 5883673448..3d5999d829 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1382,7 +1382,7 @@
identify the password as a SCRAM-SHA-256 verifier. The second field is a
salt, Base64-encoded, and the third field is the number of iterations used
to generate the password. The fourth field and fifth field are the stored
- key and server key, respectively, in hexadecimal format. A password that
+ key and server key, respectively, in Base64 format. A password that
does not follow either of those formats is assumed to be unencrypted.
</para>
</sect1>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 5077ff33b1..75a261bd36 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -371,8 +371,8 @@ scram_build_verifier(const char *username, const char *password,
int iterations)
{
uint8 keybuf[SCRAM_KEY_LEN + 1];
- char storedkey_hex[SCRAM_KEY_LEN * 2 + 1];
- char serverkey_hex[SCRAM_KEY_LEN * 2 + 1];
+ char *encoded_storedkey;
+ char *encoded_serverkey;
char salt[SCRAM_SALT_LEN];
char *encoded_salt;
int encoded_len;
@@ -403,23 +403,28 @@ scram_build_verifier(const char *username, const char *password,
encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt);
encoded_salt[encoded_len] = '\0';
- /* Calculate StoredKey, and encode it in hex */
+ /* Calculate StoredKey, and encode it in base64 */
+ encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
- (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, storedkey_hex);
- storedkey_hex[SCRAM_KEY_LEN * 2] = '\0';
+ encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
+ encoded_storedkey);
+ encoded_storedkey[encoded_len] = '\0';
/* And same for ServerKey */
+ encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
SCRAM_SERVER_KEY_NAME, keybuf);
- (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, serverkey_hex);
- serverkey_hex[SCRAM_KEY_LEN * 2] = '\0';
+ encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
+ encoded_serverkey);
+ encoded_serverkey[encoded_len] = '\0';
if (prep_password)
pfree(prep_password);
- return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations, storedkey_hex, serverkey_hex);
+ return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations,
+ encoded_storedkey, encoded_serverkey);
}
/*
@@ -538,17 +543,16 @@ parse_scram_verifier(const char *verifier, char **salt, int *iterations,
/* storedkey */
if ((p = strtok(NULL, ":")) == NULL)
goto invalid_verifier;
- if (strlen(p) != SCRAM_KEY_LEN * 2)
+ if (strlen(p) != pg_b64_enc_len(SCRAM_KEY_LEN) - 1)
goto invalid_verifier;
-
- hex_decode(p, SCRAM_KEY_LEN * 2, (char *) stored_key);
+ pg_b64_decode(p, pg_b64_enc_len(SCRAM_KEY_LEN), (char *) stored_key);
/* serverkey */
if ((p = strtok(NULL, ":")) == NULL)
goto invalid_verifier;
- if (strlen(p) != SCRAM_KEY_LEN * 2)
+ if (strlen(p) != pg_b64_enc_len(SCRAM_KEY_LEN) - 1)
goto invalid_verifier;
- hex_decode(p, SCRAM_KEY_LEN * 2, (char *) server_key);
+ pg_b64_decode(p, pg_b64_enc_len(SCRAM_KEY_LEN), (char *) server_key);
pfree(v);
return true;
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index c503e43abe..0cdb6141e2 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -23,11 +23,11 @@ CREATE ROLE regress_passwd5 PASSWORD NULL;
-- check list of created entries
--
-- The scram verifier will look something like:
--- scram-sha-256:E4HxLGtnRzsYwg==:4096:5ebc825510cb7862efd87dfa638d8337179e6913a724441dc9e888a856fbc10c:e966b1c72fad89d69aaebb156eae04edc9581286f92207c044711e79cd461bee
+-- scram-sha-256:E4HxLGtnRzsYwg==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo=
--
-- Since the salt is random, the exact value stored will be different on every test
-- run. Use a regular expression to mask the changing parts.
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
@@ -59,11 +59,11 @@ ALTER ROLE regress_passwd1 UNENCRYPTED PASSWORD 'foo'; -- unencrypted
ALTER ROLE regress_passwd2 UNENCRYPTED PASSWORD 'md5dfa155cadd5f4ad57860162f3fab9cdb'; -- encrypted with MD5
SET password_encryption = 'md5';
ALTER ROLE regress_passwd3 ENCRYPTED PASSWORD 'foo'; -- encrypted with MD5
-ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:3ded2376f7aafa93b1bdbd71bcc18b7d6ee50ed018029cc583d152ef3fc7d430:a6dd36dfc94c181956a6ae95f05e01b1864f0a22a2657d1de4ba84d2a24dc438'; -- client-supplied SCRAM verifier, use as it is
+ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; -- client-supplied SCRAM verifier, use as it is
SET password_encryption = 'scram';
ALTER ROLE regress_passwd5 ENCRYPTED PASSWORD 'foo'; -- create SCRAM verifier
CREATE ROLE regress_passwd6 ENCRYPTED PASSWORD 'md53725413363ab045e20521bf36b8d8d7f'; -- encrypted with MD5, use as it is
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql
index f4b3a9ac3a..df1ff989cc 100644
--- a/src/test/regress/sql/password.sql
+++ b/src/test/regress/sql/password.sql
@@ -24,11 +24,11 @@ CREATE ROLE regress_passwd5 PASSWORD NULL;
-- check list of created entries
--
-- The scram verifier will look something like:
--- scram-sha-256:E4HxLGtnRzsYwg==:4096:5ebc825510cb7862efd87dfa638d8337179e6913a724441dc9e888a856fbc10c:e966b1c72fad89d69aaebb156eae04edc9581286f92207c044711e79cd461bee
+-- scram-sha-256:E4HxLGtnRzsYwg==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo=
--
-- Since the salt is random, the exact value stored will be different on every test
-- run. Use a regular expression to mask the changing parts.
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
@@ -48,13 +48,13 @@ ALTER ROLE regress_passwd2 UNENCRYPTED PASSWORD 'md5dfa155cadd5f4ad57860162f3fab
SET password_encryption = 'md5';
ALTER ROLE regress_passwd3 ENCRYPTED PASSWORD 'foo'; -- encrypted with MD5
-ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:3ded2376f7aafa93b1bdbd71bcc18b7d6ee50ed018029cc583d152ef3fc7d430:a6dd36dfc94c181956a6ae95f05e01b1864f0a22a2657d1de4ba84d2a24dc438'; -- client-supplied SCRAM verifier, use as it is
+ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; -- client-supplied SCRAM verifier, use as it is
SET password_encryption = 'scram';
ALTER ROLE regress_passwd5 ENCRYPTED PASSWORD 'foo'; -- create SCRAM verifier
CREATE ROLE regress_passwd6 ENCRYPTED PASSWORD 'md53725413363ab045e20521bf36b8d8d7f'; -- encrypted with MD5, use as it is
-SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid
WHERE rolname LIKE 'regress_passwd%'
ORDER BY rolname, rolpassword;
--
2.12.2
0002-Move-routine-to-build-SCRAM-verifier-into-src-common.patchapplication/octet-stream; name=0002-Move-routine-to-build-SCRAM-verifier-into-src-common.patchDownload
From b6ed8cfed14fa3eb272ec514f5e5a7d46944cf99 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 10 Apr 2017 14:33:51 +0900
Subject: [PATCH 2/5] Move routine to build SCRAM verifier into src/common/
This is aimed at being used by libpq to allow frontend-side creation
of SCRAM verifiers. The result is not anymore allocated by the routine
itself, the caller is responsible for that, similarly to md5.
---
src/backend/libpq/auth-scram.c | 79 ++++++---------------------------------
src/backend/libpq/crypt.c | 21 ++++++++++-
src/common/scram-common.c | 78 ++++++++++++++++++++++++++++++++++++++
src/include/common/scram-common.h | 20 ++++++++++
src/include/libpq/scram.h | 5 +--
5 files changed, 129 insertions(+), 74 deletions(-)
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 75a261bd36..aa05662af2 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -204,9 +204,18 @@ pg_be_scram_init(const char *username, const char *shadow_pass)
* The stored password is in plain format. Generate a fresh SCRAM
* verifier from it, and proceed with that.
*/
- char *verifier;
+ char *verifier = palloc(SCRAM_VERIFIER_LEN + 1);
+ char salt[SCRAM_SALT_LEN];
- verifier = scram_build_verifier(username, shadow_pass, 0);
+ if (!pg_backend_random(salt, SCRAM_SALT_LEN))
+ {
+ ereport(LOG,
+ (errcode(ERRCODE_INTERNAL_ERROR),
+ errmsg("could not generate random salt")));
+ return NULL;
+ }
+
+ (void) scram_build_verifier(username, shadow_pass, salt, 0, verifier);
(void) parse_scram_verifier(verifier, &state->salt, &state->iterations,
state->StoredKey, state->ServerKey);
@@ -360,72 +369,6 @@ pg_be_scram_exchange(void *opaq, char *input, int inputlen,
return result;
}
-/*
- * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
- *
- * If iterations is 0, default number of iterations is used. The result is
- * palloc'd, so caller is responsible for freeing it.
- */
-char *
-scram_build_verifier(const char *username, const char *password,
- int iterations)
-{
- uint8 keybuf[SCRAM_KEY_LEN + 1];
- char *encoded_storedkey;
- char *encoded_serverkey;
- char salt[SCRAM_SALT_LEN];
- char *encoded_salt;
- int encoded_len;
- char *prep_password = NULL;
- pg_saslprep_rc rc;
-
- /*
- * Normalize the password with SASLprep. If that doesn't work, because
- * the password isn't valid UTF-8 or contains prohibited characters, just
- * proceed with the original password. (See comments at top of file.)
- */
- rc = pg_saslprep(password, &prep_password);
- if (rc == SASLPREP_SUCCESS)
- password = (const char *) prep_password;
-
- if (iterations <= 0)
- iterations = SCRAM_ITERATIONS_DEFAULT;
-
- if (!pg_backend_random(salt, SCRAM_SALT_LEN))
- {
- ereport(LOG,
- (errcode(ERRCODE_INTERNAL_ERROR),
- errmsg("could not generate random salt")));
- return NULL;
- }
-
- encoded_salt = palloc(pg_b64_enc_len(SCRAM_SALT_LEN) + 1);
- encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt);
- encoded_salt[encoded_len] = '\0';
-
- /* Calculate StoredKey, and encode it in base64 */
- encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
- scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
- iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
- scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
- encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
- encoded_storedkey);
- encoded_storedkey[encoded_len] = '\0';
-
- /* And same for ServerKey */
- encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
- scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
- SCRAM_SERVER_KEY_NAME, keybuf);
- encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
- encoded_serverkey);
- encoded_serverkey[encoded_len] = '\0';
-
- if (prep_password)
- pfree(prep_password);
-
- return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations,
- encoded_storedkey, encoded_serverkey);
-}
/*
* Verify a plaintext password against a SCRAM verifier. This is used when
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 34beab5334..a0426f5450 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,9 +20,11 @@
#include "catalog/pg_authid.h"
#include "common/md5.h"
+#include "common/scram-common.h"
#include "libpq/crypt.h"
#include "libpq/scram.h"
#include "miscadmin.h"
+#include "utils/backend_random.h"
#include "utils/builtins.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
@@ -156,8 +158,23 @@ encrypt_password(PasswordType target_type, const char *role,
switch (guessed_type)
{
case PASSWORD_TYPE_PLAINTEXT:
- return scram_build_verifier(role, password, 0);
-
+ {
+ char salt[SCRAM_SALT_LEN];
+
+ if (!pg_backend_random(salt, SCRAM_SALT_LEN))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INTERNAL_ERROR),
+ errmsg("could not generate random salt")));
+ return NULL;
+ }
+
+ encrypted_password = palloc(SCRAM_VERIFIER_LEN + 1);
+ if (!scram_build_verifier(role, password, salt, 0,
+ encrypted_password))
+ elog(ERROR, "password encryption failed");
+ return encrypted_password;
+ }
case PASSWORD_TYPE_MD5:
/*
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index df9f0eaa90..2e5ec0c6c0 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -23,6 +23,7 @@
#include <netinet/in.h>
#include <arpa/inet.h>
+#include "common/saslprep.h"
#include "common/scram-common.h"
#define HMAC_IPAD 0x36
@@ -165,3 +166,80 @@ scram_ClientOrServerKey(const char *password,
scram_HMAC_update(&ctx, keystr, strlen(keystr));
scram_HMAC_final(result, &ctx);
}
+
+/*
+ * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
+ *
+ * If iterations is 0, default number of iterations is used. The result is
+ * stored in "verifier" that caller is responsible to allocate a buffer of
+ * size SCRAM_VERIFIER_LEN. Returns true if the verifier has been generated,
+ * false otherwise. It is important for this routine to do no memory
+ * allocations (SASLprep is an exception to that as the prepared password's
+ * length is estimated locally). The salt defined by the caller needs to be a
+ * buffer pre-filled with random data of length SCRAM_SALT_LEN.
+ */
+bool
+scram_build_verifier(const char *username, const char *password,
+ const char *salt, int iterations, char *verifier)
+{
+ uint8 keybuf[SCRAM_KEY_LEN + 1];
+ char intbuf[12];
+ char *p;
+ char *prep_password = NULL;
+ pg_saslprep_rc rc;
+
+ /*
+ * Normalize the password with SASLprep. If that doesn't work, because
+ * the password isn't valid UTF-8 or contains prohibited characters, just
+ * proceed with the original password. (See comments at top of
+ * auth-scram.c.)
+ */
+ rc = pg_saslprep(password, &prep_password);
+ if (rc == SASLPREP_SUCCESS)
+ password = (const char *) prep_password;
+
+ if (iterations <= 0)
+ iterations = SCRAM_ITERATIONS_DEFAULT;
+
+ /* Fill in the data of the verifier */
+ p = verifier;
+ memcpy(p, SCRAM_VERIFIER_PREFIX, strlen(SCRAM_VERIFIER_PREFIX));
+ p += strlen(SCRAM_VERIFIER_PREFIX);
+ *p++ = ':';
+
+ /* salt */
+ (void) pg_b64_encode(salt, SCRAM_SALT_LEN, p);
+ p += pg_b64_enc_len(SCRAM_SALT_LEN);
+ *p++ = ':';
+
+ /* iterations */
+ sprintf(intbuf, "%d", iterations);
+ memcpy(p, intbuf, strlen(intbuf));
+ p += strlen(intbuf);
+ *p++ = ':';
+
+ /* Calculate StoredKey, and encode it in base64 */
+ scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
+ iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
+ scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
+ (void) pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, p);
+ p += pg_b64_enc_len(SCRAM_KEY_LEN) - 1;
+ *p++ = ':';
+
+ /* And same for ServerKey */
+ scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
+ SCRAM_SERVER_KEY_NAME, keybuf);
+ (void) pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, p);
+ p += pg_b64_enc_len(SCRAM_KEY_LEN) - 1;
+ *p++ = '\0';
+
+#ifndef FRONTEND
+ if (prep_password)
+ pfree(prep_password);
+#else
+ if (prep_password)
+ free(prep_password);
+#endif
+
+ return true;
+}
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index 6740069eee..fcf3b98ba6 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -13,6 +13,7 @@
#ifndef SCRAM_COMMON_H
#define SCRAM_COMMON_H
+#include "common/base64.h"
#include "common/sha2.h"
/* Length of SCRAM keys (client and server) */
@@ -38,6 +39,22 @@
#define SCRAM_SERVER_KEY_NAME "Server Key"
#define SCRAM_CLIENT_KEY_NAME "Client Key"
+#define SCRAM_VERIFIER_PREFIX "scram-sha-256"
+
+/*
+ * Length of a SCRAM verifier, which is made of the following five fields
+ * separated by a colon:
+ * - prefix "scram-sha-256", made of 13 characters.
+ * - 4 colon separators.
+ * - 32-bit number of interations, up to 11 characters.
+ * - base64-encoded salt of length SCRAM_SALT_LEN
+ * - base64-encoded stored key of length SCRAM_KEY_LEN
+ * - base64-encoded server key of length SCRAM_KEY_LEN
+ */
+#define SCRAM_VERIFIER_LEN (strlen("scram-sha-256") + 4 + 11 + \
+ pg_b64_enc_len(SCRAM_SALT_LEN) + \
+ pg_b64_enc_len(SCRAM_KEY_LEN) * 2)
+
/*
* Context data for HMAC used in SCRAM authentication.
*/
@@ -55,5 +72,8 @@ extern void scram_H(const uint8 *str, int len, uint8 *result);
extern void scram_ClientOrServerKey(const char *password, const char *salt,
int saltlen, int iterations,
const char *keystr, uint8 *result);
+extern bool scram_build_verifier(const char *username, const char *password,
+ const char *salt, int iterations,
+ char *verifier);
#endif /* SCRAM_COMMON_H */
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index e373f0c07e..803fc4ef7a 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -26,10 +26,7 @@ extern void *pg_be_scram_init(const char *username, const char *shadow_pass);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
-/* Routines to handle and check SCRAM-SHA-256 verifier */
-extern char *scram_build_verifier(const char *username,
- const char *password,
- int iterations);
+/* Routines to check SCRAM-SHA-256 verifier */
extern bool is_scram_verifier(const char *verifier);
extern bool scram_verify_plain_password(const char *username,
const char *password, const char *verifier);
--
2.12.2
0003-Refactor-frontend-side-random-number-generation.patchapplication/octet-stream; name=0003-Refactor-frontend-side-random-number-generation.patchDownload
From e213a58dc4f4db062e3b589cc0bc70005d92dbab Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 7 Apr 2017 10:24:44 +0900
Subject: [PATCH 3/5] Refactor frontend-side random number generation
pg_frontend_random() is moved into its own file in libpq/ to give other
portions of the libpq code the ability to generate random numbers. This
will be used for the SCRAM verifier generation from clients.
---
src/interfaces/libpq/Makefile | 2 +-
src/interfaces/libpq/fe-auth-scram.c | 53 -----------------------
src/interfaces/libpq/libpq-int.h | 1 +
src/interfaces/libpq/libpq-random.c | 84 ++++++++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-random.h | 17 ++++++++
src/tools/msvc/Install.pm | 2 +-
6 files changed, 104 insertions(+), 55 deletions(-)
create mode 100644 src/interfaces/libpq/libpq-random.c
create mode 100644 src/interfaces/libpq/libpq-random.h
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 87f22d242f..eb6777ef2b 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -33,7 +33,7 @@ LIBS := $(LIBS:-lpgport=)
# OBJS from this file.
OBJS= fe-auth.o fe-auth-scram.o fe-connect.o fe-exec.o fe-misc.o fe-print.o fe-lobj.o \
fe-protocol2.o fe-protocol3.o pqexpbuffer.o fe-secure.o \
- libpq-events.o
+ libpq-events.o libpq-random.o
# libpgport C files we always use
OBJS += chklocale.o inet_net_ntop.o noblock.o pgstrcasecmp.o pqsignal.o \
thread.o
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index c56e91e0e0..b453c6cc21 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -74,7 +74,6 @@ static bool verify_server_proof(fe_scram_state *state);
static void calculate_client_proof(fe_scram_state *state,
const char *client_final_message_without_proof,
uint8 *result);
-static bool pg_frontend_random(char *dst, int len);
/*
* Initialize SCRAM exchange status.
@@ -609,55 +608,3 @@ verify_server_proof(fe_scram_state *state)
return true;
}
-
-/*
- * Random number generator.
- */
-static bool
-pg_frontend_random(char *dst, int len)
-{
-#ifdef HAVE_STRONG_RANDOM
- return pg_strong_random(dst, len);
-#else
- int i;
- char *end = dst + len;
-
- static unsigned short seed[3];
- static int mypid = 0;
-
- pglock_thread();
-
- if (mypid != getpid())
- {
- struct timeval now;
-
- gettimeofday(&now, NULL);
-
- seed[0] = now.tv_sec ^ getpid();
- seed[1] = (unsigned short) (now.tv_usec);
- seed[2] = (unsigned short) (now.tv_usec >> 16);
- }
-
- for (i = 0; dst < end; i++)
- {
- uint32 r;
- int j;
-
- /*
- * pg_jrand48 returns a 32-bit integer. Fill the next 4 bytes from
- * it.
- */
- r = (uint32) pg_jrand48(seed);
-
- for (j = 0; j < 4 && dst < end; j++)
- {
- *(dst++) = (char) (r & 0xFF);
- r >>= 8;
- }
- }
-
- pgunlock_thread();
-
- return true;
-#endif
-}
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index b8ec3418c5..6b72a17a75 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -22,6 +22,7 @@
/* We assume libpq-fe.h has already been included. */
#include "libpq-events.h"
+#include "libpq-random.h"
#include <time.h>
#ifndef WIN32
diff --git a/src/interfaces/libpq/libpq-random.c b/src/interfaces/libpq/libpq-random.c
new file mode 100644
index 0000000000..5e788dc821
--- /dev/null
+++ b/src/interfaces/libpq/libpq-random.c
@@ -0,0 +1,84 @@
+/*-------------------------------------------------------------------------
+ *
+ * libpq-random.c
+ * Frontend random number generation routine for libpq.
+ *
+ * pg_frontend_random() function fills a buffer with random bytes. Normally,
+ * it is just a thin wrapper around pg_strong_random(), but when compiled
+ * with --disable-strong-random, there is a built-in implementation.
+ *
+ * The built-in implementation uses the standard erand48 algorithm, with
+ * a seed calculated using the process ID and a timestamp.
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/interfaces/libpq/libpq-random.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+/* These are needed for getpid(), in the fallback implementation */
+#ifndef HAVE_STRONG_RANDOM
+#include <sys/types.h>
+#include <unistd.h>
+#endif
+
+/*
+ * Random number generator.
+ */
+bool
+pg_frontend_random(char *dst, int len)
+{
+#ifdef HAVE_STRONG_RANDOM
+ return pg_strong_random(dst, len);
+#else
+ int i;
+ char *end = dst + len;
+
+ static unsigned short seed[3];
+ static int mypid = 0;
+
+ pglock_thread();
+
+ if (mypid != getpid())
+ {
+ struct timeval now;
+
+ gettimeofday(&now, NULL);
+
+ seed[0] = now.tv_sec ^ getpid();
+ seed[1] = (unsigned short) (now.tv_usec);
+ seed[2] = (unsigned short) (now.tv_usec >> 16);
+ }
+
+ for (i = 0; dst < end; i++)
+ {
+ uint32 r;
+ int j;
+
+ /*
+ * pg_jrand48 returns a 32-bit integer. Fill the next 4 bytes from
+ * it.
+ */
+ r = (uint32) pg_jrand48(seed);
+
+ for (j = 0; j < 4 && dst < end; j++)
+ {
+ *(dst++) = (char) (r & 0xFF);
+ r >>= 8;
+ }
+ }
+
+ pgunlock_thread();
+
+ return true;
+#endif
+}
diff --git a/src/interfaces/libpq/libpq-random.h b/src/interfaces/libpq/libpq-random.h
new file mode 100644
index 0000000000..054998b8d9
--- /dev/null
+++ b/src/interfaces/libpq/libpq-random.h
@@ -0,0 +1,17 @@
+/*-------------------------------------------------------------------------
+ *
+ * frontend_random.h
+ * Declarations for frontend random number generation
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ *
+ * src/interfaces/libpq/libpq-random.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef LIBPQ_RANDOM_H
+#define LIBPQ_RANDOM_H
+
+extern bool pg_frontend_random(char *dst, int len);
+
+#endif /* LIBPQ_RANDOM_H */
diff --git a/src/tools/msvc/Install.pm b/src/tools/msvc/Install.pm
index 35ad5b8a44..4acd6592ad 100644
--- a/src/tools/msvc/Install.pm
+++ b/src/tools/msvc/Install.pm
@@ -601,7 +601,7 @@ sub CopyIncludeFiles
CopyFiles(
'Libpq internal headers',
$target . '/include/internal/',
- 'src/interfaces/libpq/', 'libpq-int.h', 'pqexpbuffer.h');
+ 'src/interfaces/libpq/', 'libpq-int.h', 'libpq-random.h', 'pqexpbuffer.h');
CopyFiles(
'Internal headers',
--
2.12.2
0004-Extend-PQencryptPassword-with-a-hashing-method.patchapplication/octet-stream; name=0004-Extend-PQencryptPassword-with-a-hashing-method.patchDownload
From 47f7fb16724ee43e36fe93256fc67b0fbbde4cd1 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 7 Apr 2017 10:39:27 +0900
Subject: [PATCH 4/5] Extend PQencryptPassword with a hashing method
This extra argument can use the following values when hashing the
password:
- scram, for SCRAM-SHA-256 hashing.
- md5, for MD5 hashing.
- plain, for cleartext.
---
doc/src/sgml/libpq.sgml | 6 ++++-
src/bin/psql/command.c | 2 +-
src/bin/scripts/createuser.c | 3 ++-
src/interfaces/libpq/fe-auth.c | 54 ++++++++++++++++++++++++++++++++---------
src/interfaces/libpq/libpq-fe.h | 3 ++-
5 files changed, 52 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 4bc5bf3192..fc1aa4b5e5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -5887,7 +5887,8 @@ void PQconninfoFree(PQconninfoOption *connOptions);
<para>
Prepares the encrypted form of a <productname>PostgreSQL</> password.
<synopsis>
-char * PQencryptPassword(const char *passwd, const char *user);
+char * PQencryptPassword(const char *passwd, const char *user,
+ const char *method);
</synopsis>
This function is intended to be used by client applications that
wish to send commands like <literal>ALTER USER joe PASSWORD
@@ -5901,6 +5902,9 @@ char * PQencryptPassword(const char *passwd, const char *user);
memory. The caller can assume the string doesn't contain any
special characters that would require escaping. Use
<function>PQfreemem</> to free the result when done with it.
+ The encryption method of the password can be specified as
+ <literal>md5</> for hashing with MD5, <literal>scram</> for
+ hashing with SCRAM-SHA-256 and <literal>plain</> for cleartext.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 494f468575..9716c98fc2 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1882,7 +1882,7 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
else
user = PQuser(pset.db);
- encrypted_password = PQencryptPassword(pw1, user);
+ encrypted_password = PQencryptPassword(pw1, user, "md5");
if (!encrypted_password)
{
diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c
index 3d74797a8f..5af263e34a 100644
--- a/src/bin/scripts/createuser.c
+++ b/src/bin/scripts/createuser.c
@@ -275,7 +275,8 @@ main(int argc, char *argv[])
char *encrypted_password;
encrypted_password = PQencryptPassword(newpassword,
- newuser);
+ newuser,
+ "md5");
if (!encrypted_password)
{
fprintf(stderr, _("Password encryption failed.\n"));
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 5fe7e565a0..40d4be3ca6 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -39,6 +39,7 @@
#endif
#include "common/md5.h"
+#include "common/scram-common.h"
#include "libpq-fe.h"
#include "libpq/scram.h"
#include "fe-auth.h"
@@ -919,27 +920,56 @@ pg_fe_getauthname(PQExpBuffer errorMessage)
* be dependent on low-level details like whether the encryption is MD5
* or something else.
*
- * Arguments are the cleartext password, and the SQL name of the user it
- * is for.
+ * Arguments are the cleartext password, the SQL name of the user it
+ * is for, and the name of password hashing method:
+ * - "scram", to hash password using SCRAM-SHA-256.
+ * - "md5", to hash password using MD5.
+ * - "plain", to get a cleartext value of password.
*
- * Return value is a malloc'd string, or NULL if out-of-memory. The client
- * may assume the string doesn't contain any special characters that would
- * require escaping.
+ * Return value is a malloc'd string, or NULL if out-of-memory or in
+ * the event of an error. The client may assume the string doesn't
+ * contain any special characters that would require escaping.
*/
char *
-PQencryptPassword(const char *passwd, const char *user)
+PQencryptPassword(const char *passwd, const char *user, const char *method)
{
char *crypt_pwd;
- crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
- if (!crypt_pwd)
- return NULL;
+ if (strcmp(method, "md5") == 0)
+ {
+ crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
+ if (!crypt_pwd)
+ return NULL;
- if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ {
+ free(crypt_pwd);
+ return NULL;
+ }
+ }
+ else if (strcmp(method, "scram") == 0)
{
- free(crypt_pwd);
- return NULL;
+ char salt[SCRAM_SALT_LEN];
+
+ crypt_pwd = malloc(SCRAM_VERIFIER_LEN + 1);
+ if (!crypt_pwd)
+ return NULL;
+
+ if (!pg_frontend_random(salt, SCRAM_SALT_LEN))
+ return NULL;
+
+ if (!scram_build_verifier(user, passwd, salt, 0, crypt_pwd))
+ {
+ free(crypt_pwd);
+ return NULL;
+ }
}
+ else if (strcmp(method, "plain") == 0)
+ {
+ crypt_pwd = strdup(passwd);
+ }
+ else
+ return NULL;
return crypt_pwd;
}
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 635af5b50e..c312dd0152 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -596,7 +596,8 @@ extern int PQenv2encoding(void);
/* === in fe-auth.c === */
-extern char *PQencryptPassword(const char *passwd, const char *user);
+extern char *PQencryptPassword(const char *passwd, const char *user,
+ const char *method);
/* === in encnames.c === */
--
2.12.2
0005-Extend-psql-s-password-and-createuser-to-handle-SCRA.patchapplication/octet-stream; name=0005-Extend-psql-s-password-and-createuser-to-handle-SCRA.patchDownload
From 7d527d12518501a68ceab2fec7dc1abc90e7272d Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Sat, 25 Mar 2017 14:07:16 +0900
Subject: [PATCH 5/5] Extend psql's \password and createuser to handle SCRAM
verifier creation
Depending on the version of PostgreSQL those utilities are connected to,
generate MD5 verifiers when connecting to a server older than 10, and
MD5 otherwise.
---
doc/src/sgml/ref/createuser.sgml | 6 ++++--
doc/src/sgml/ref/psql-ref.sgml | 4 +++-
src/bin/psql/command.c | 9 ++++++++-
src/bin/scripts/createuser.c | 16 +++++++++++++---
4 files changed, 28 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml
index 4332008c68..4454591d79 100644
--- a/doc/src/sgml/ref/createuser.sgml
+++ b/doc/src/sgml/ref/createuser.sgml
@@ -125,7 +125,9 @@ PostgreSQL documentation
<listitem>
<para>
Encrypts the user's password stored in the database. If not
- specified, the default password behavior is used.
+ specified, the default password behavior is used. The password
+ is hashed using SCRAM-SHA-256 when connecting to a version of
+ <productname>PostgreSQL</> newer than 10, and MD5 otherwise.
</para>
</listitem>
</varlistentry>
@@ -477,7 +479,7 @@ PostgreSQL documentation
<prompt>$ </prompt><userinput>createuser -P -s -e joe</userinput>
<computeroutput>Enter password for new role: </computeroutput><userinput>xyzzy</userinput>
<computeroutput>Enter it again: </computeroutput><userinput>xyzzy</userinput>
-<computeroutput>CREATE ROLE joe PASSWORD 'md5b5f5ba1a423792b526f799ae4eb3d59e' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;</computeroutput>
+<computeroutput>CREATE ROLE joe PASSWORD 'scram-sha-256:aPheg4yCAFhStg==:4096:TOHLPF8w+NzqtcxD2oz9w5wPISutHLOXWMKoe4HCvuo=:lTG0OiB/ZH5/hsfUqnndRwfMziY5j5C6FS8IAIwL2nA=' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;</computeroutput>
</screen>
In the above example, the new password isn't actually echoed when typed,
but we show what was typed for clarity. As you see, the password is
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 3b86612862..749221aa76 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2380,7 +2380,9 @@ lo_import 152801
user). This command prompts for the new password, encrypts it, and
sends it to the server as an <command>ALTER ROLE</> command. This
makes sure that the new password does not appear in cleartext in the
- command history, the server log, or elsewhere.
+ command history, the server log, or elsewhere. The password is hashed
+ with SCRAM-SHA-256 when connecting to <productname>PostgreSQL</> 10
+ and newer versions, and with MD5 otherwise.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 9716c98fc2..4e9b526e43 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1882,7 +1882,14 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
else
user = PQuser(pset.db);
- encrypted_password = PQencryptPassword(pw1, user, "md5");
+ /*
+ * Hash password using SCRAM-SHA-256 when connecting to servers
+ * newer than Postgres 10, and hash with MD5 otherwise.
+ */
+ if (pset.sversion < 100000)
+ encrypted_password = PQencryptPassword(pw1, user, "md5");
+ else
+ encrypted_password = PQencryptPassword(pw1, user, "scram");
if (!encrypted_password)
{
diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c
index 5af263e34a..0107497e23 100644
--- a/src/bin/scripts/createuser.c
+++ b/src/bin/scripts/createuser.c
@@ -274,9 +274,19 @@ main(int argc, char *argv[])
{
char *encrypted_password;
- encrypted_password = PQencryptPassword(newpassword,
- newuser,
- "md5");
+ /*
+ * Hash password using SCRAM-SHA-256 when connecting to servers
+ * newer than Postgres 10, and hash with MD5 otherwise.
+ */
+ if (PQserverVersion(conn) < 100000)
+ encrypted_password = PQencryptPassword(newpassword,
+ newuser,
+ "md5");
+ else
+ encrypted_password = PQencryptPassword(newpassword,
+ newuser,
+ "scram");
+
if (!encrypted_password)
{
fprintf(stderr, _("Password encryption failed.\n"));
--
2.12.2
On 04/10/2017 08:42 AM, Michael Paquier wrote:
As there have been some conflicts because of the commit of SASLprep, here is a rebased set of patches. A couple of things worth noting: - SASLprep does an allocation of the prepared password string. It is definitely better to do all the ground work in pg_saslprep but this costs a free() call with a #ifdef FRONTEND at the end of scram_build_verifier(). - Patch 0005 does that: + /* + * Hash password using SCRAM-SHA-256 when connecting to servers + * newer than Postgres 10, and hash with MD5 otherwise. + */ + if (pset.sversion < 100000) + encrypted_password = PQencryptPassword(pw1, user, "md5"); + else + encrypted_password = PQencryptPassword(pw1, user, "scram"); Actually I am thinking that guessing the hashing function according to the value of password_encryption would make the most sense. Thoughts?
Thanks! I've been busy on the other thread on future-proofing the
protocol with negotiating the SASL mechanism, I'll come back to this
once we get that settled. By the end of the week, I presume.
Not sure about the password-encryption thing, there are good arguments
for either behavior..
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Apr 11, 2017 at 10:07:12PM +0300, Heikki Linnakangas wrote:
On 04/10/2017 08:42 AM, Michael Paquier wrote:
As there have been some conflicts because of the commit of SASLprep, here is a rebased set of patches. A couple of things worth noting: - SASLprep does an allocation of the prepared password string. It is definitely better to do all the ground work in pg_saslprep but this costs a free() call with a #ifdef FRONTEND at the end of scram_build_verifier(). - Patch 0005 does that: + /* + * Hash password using SCRAM-SHA-256 when connecting to servers + * newer than Postgres 10, and hash with MD5 otherwise. + */ + if (pset.sversion < 100000) + encrypted_password = PQencryptPassword(pw1, user, "md5"); + else + encrypted_password = PQencryptPassword(pw1, user, "scram"); Actually I am thinking that guessing the hashing function according to the value of password_encryption would make the most sense. Thoughts?Thanks! I've been busy on the other thread on future-proofing the protocol
with negotiating the SASL mechanism, I'll come back to this once we get that
settled. By the end of the week, I presume.
This PostgreSQL 10 open item is past due for your status update. Kindly send
a status update within 24 hours, and include a date for your subsequent status
update. Refer to the policy on open item ownership:
/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 14 March 2017 at 15:40, Tom Lane <tgl@sss.pgh.pa.us> wrote:
I was also thinking about that. Basically a primary method and a
fallback. If that were the case, a gradual transition could happen, and
if we want \password to enforce best practice it would be ok.Why exactly would anyone want "md5 only"? I should think that "scram
only" is a sensible pg_hba setting, if the DBA feels that md5 is too
insecure, but I do not see the point of "md5 only" in 2017. I think
we should just start interpreting that as "md5 or better".
+1
As a potential open item, if we treat "md5" as ">= md5"
should we not also treat "password" as ">=password"?
It seems strange that we still support "password" and yet tell
everyonenot to use it.
I'd like PG10 to be the version where I don't have to tell people not
to use certain things, hash indexes, "password" etc.
--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 04/18/2017 11:15 AM, Simon Riggs wrote:
As a potential open item, if we treat "md5" as ">= md5"
should we not also treat "password" as ">=password"?It seems strange that we still support "password" and yet tell
everyonenot to use it.I'd like PG10 to be the version where I don't have to tell people not
to use certain things, hash indexes, "password" etc.
Between md5 and scram, the choice is easy, because a user can only have
an MD5 hashed or SCRAM "hashed" password in pg_authid. So you present
the client an MD5 challenge or a SCRAM challenge, depending on what the
user has in pg_authid, or you error out without even trying. But
"password" authentication can be used with any kind of a verifier in
pg_authid. "password" authentication can be useful, for example, if a
user has a SCRAM verifier in pg_authid, but the client doesn't support
SCRAM.
You could argue that you shouldn't use it even in that situation, you
should upgrade the client, or use SSL certs or an ssh tunnel or
something else instead. But that's a very different argument than the
one for treating "md5" as ">= md5".
Also note that LDAP and RADIUS authentication look identical to
"password" authentication, on the wire. The only difference is that
instead of checking the password against pg_authid, the server checks it
against an LDAP or RADIUS server.
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 04/18/2017 08:44 AM, Noah Misch wrote:
On Tue, Apr 11, 2017 at 10:07:12PM +0300, Heikki Linnakangas wrote:
Thanks! I've been busy on the other thread on future-proofing the protocol
with negotiating the SASL mechanism, I'll come back to this once we get that
settled. By the end of the week, I presume.This PostgreSQL 10 open item is past due for your status update. Kindly send
a status update within 24 hours, and include a date for your subsequent status
update. Refer to the policy on open item ownership:
/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com
Took a bit longer than I predicted, but the other scram open items have
now been resolved, and I'll start reviewing this now. I expect there
will be 1-2 iterations on this before it's committed. I'll send another
status update by Friday, if this isn't committed by then.
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 18 April 2017 at 09:25, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 04/18/2017 11:15 AM, Simon Riggs wrote:
As a potential open item, if we treat "md5" as ">= md5"
should we not also treat "password" as ">=password"?It seems strange that we still support "password" and yet tell
everyonenot to use it.I'd like PG10 to be the version where I don't have to tell people not
to use certain things, hash indexes, "password" etc.Between md5 and scram, the choice is easy, because a user can only have an
MD5 hashed or SCRAM "hashed" password in pg_authid. So you present the
client an MD5 challenge or a SCRAM challenge, depending on what the user has
in pg_authid, or you error out without even trying. But "password"
authentication can be used with any kind of a verifier in pg_authid.
"password" authentication can be useful, for example, if a user has a SCRAM
verifier in pg_authid, but the client doesn't support SCRAM.
Which would be a little strange and defeat the purpose of SCRAM.
You could argue that you shouldn't use it even in that situation, you should
upgrade the client, or use SSL certs or an ssh tunnel or something else
instead. But that's a very different argument than the one for treating
"md5" as ">= md5".Also note that LDAP and RADIUS authentication look identical to "password"
authentication, on the wire. The only difference is that instead of checking
the password against pg_authid, the server checks it against an LDAP or
RADIUS server.
So the argument is multiple things are dangerous so we do nothing...
We have an opportunity to change things because its PG10, so lets not
waste the opportunity.
Thanks very much for working on SCRAM, its a good feature.
--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 04/10/2017 08:42 AM, Michael Paquier wrote:
As there have been some conflicts because of the commit of SASLprep,
here is a rebased set of patches.
I've committed a modified version of the first patch, to change the
on-disk format to RFC 5803, as mentioned on the other thread
(/messages/by-id/351ba574-85ea-d9b8-9689-8c928dd0955d@iki.fi).
I'll continue reviewing the rest of the patch on Monday, but one glaring
issue is that we cannot add an argument to the existing libpq
PQencryptPassword() function, because that's part of the public API. It
would break all applications that use PQencryptPassword().
What we need to do is to add a new function. What should we call that?
We don't have a tradition of using "Ex" or such suffix to mark extended
versions of existing functions. PQencryptPasswordWithMethod(user, pass,
method) ?
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sat, Apr 22, 2017 at 5:04 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
I'll continue reviewing the rest of the patch on Monday, but one glaring
issue is that we cannot add an argument to the existing libpq
PQencryptPassword() function, because that's part of the public API. It
would break all applications that use PQencryptPassword().What we need to do is to add a new function. What should we call that? We
don't have a tradition of using "Ex" or such suffix to mark extended
versions of existing functions. PQencryptPasswordWithMethod(user, pass,
method) ?
Do we really want to add a new function or have a hard failure? Any
application calling PQencryptPassword may trap itself silently if the
server uses scram as hba key or if the default is switched to that,
from this point of view extending the function makes sense as well.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sat, Apr 22, 2017 at 7:20 AM, Michael Paquier
<michael.paquier@gmail.com> wrote:
Do we really want to add a new function or have a hard failure? Any
application calling PQencryptPassword may trap itself silently if the
server uses scram as hba key or if the default is switched to that,
from this point of view extending the function makes sense as well.
Attached is a new patch set, taking into account the new format.
--
Michael
Attachments:
0001-Move-routine-to-build-SCRAM-verifier-into-src-common.patchapplication/octet-stream; name=0001-Move-routine-to-build-SCRAM-verifier-into-src-common.patchDownload
From 371c057f7bbf6531c0e1d9850c411e64d4a0a6e5 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 25 Apr 2017 13:58:40 +0900
Subject: [PATCH 1/4] Move routine to build SCRAM verifier into src/common/
This is aimed at being used by libpq to allow frontend-side creation
of SCRAM verifiers. The result is not anymore allocated by the routine
itself, the caller is responsible for that, similarly to md5.
---
src/backend/libpq/auth-scram.c | 91 ++++++---------------------------------
src/backend/libpq/crypt.c | 21 ++++++++-
src/common/scram-common.c | 78 +++++++++++++++++++++++++++++++++
src/include/common/scram-common.h | 20 +++++++++
src/include/libpq/scram.h | 5 +--
5 files changed, 130 insertions(+), 85 deletions(-)
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 16bea446e3..1153631666 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -205,9 +205,18 @@ pg_be_scram_init(const char *username, const char *shadow_pass)
* The stored password is in plain format. Generate a fresh SCRAM
* verifier from it, and proceed with that.
*/
- char *verifier;
+ char *verifier = palloc(SCRAM_VERIFIER_LEN + 1);
+ char salt[SCRAM_SALT_LEN];
- verifier = scram_build_verifier(username, shadow_pass, 0);
+ if (!pg_backend_random(salt, SCRAM_SALT_LEN))
+ {
+ ereport(LOG,
+ (errcode(ERRCODE_INTERNAL_ERROR),
+ errmsg("could not generate random salt")));
+ return NULL;
+ }
+
+ (void) scram_build_verifier(username, shadow_pass, salt, 0, verifier);
(void) parse_scram_verifier(verifier, &state->iterations, &state->salt,
state->StoredKey, state->ServerKey);
@@ -385,82 +394,6 @@ pg_be_scram_exchange(void *opaq, char *input, int inputlen,
}
/*
- * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
- *
- * If iterations is 0, default number of iterations is used. The result is
- * palloc'd, so caller is responsible for freeing it.
- */
-char *
-scram_build_verifier(const char *username, const char *password,
- int iterations)
-{
- char *prep_password = NULL;
- pg_saslprep_rc rc;
- char saltbuf[SCRAM_SALT_LEN];
- uint8 keybuf[SCRAM_KEY_LEN];
- char *encoded_salt;
- char *encoded_storedkey;
- char *encoded_serverkey;
- int encoded_len;
- char *result;
-
- /*
- * Normalize the password with SASLprep. If that doesn't work, because
- * the password isn't valid UTF-8 or contains prohibited characters, just
- * proceed with the original password. (See comments at top of file.)
- */
- rc = pg_saslprep(password, &prep_password);
- if (rc == SASLPREP_SUCCESS)
- password = (const char *) prep_password;
-
- if (iterations <= 0)
- iterations = SCRAM_ITERATIONS_DEFAULT;
-
- /* Generate salt, and encode it in base64 */
- if (!pg_backend_random(saltbuf, SCRAM_SALT_LEN))
- {
- ereport(LOG,
- (errcode(ERRCODE_INTERNAL_ERROR),
- errmsg("could not generate random salt")));
- return NULL;
- }
-
- encoded_salt = palloc(pg_b64_enc_len(SCRAM_SALT_LEN) + 1);
- encoded_len = pg_b64_encode(saltbuf, SCRAM_SALT_LEN, encoded_salt);
- encoded_salt[encoded_len] = '\0';
-
- /* Calculate StoredKey, and encode it in base64 */
- scram_ClientOrServerKey(password, saltbuf, SCRAM_SALT_LEN,
- iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
- scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
-
- encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
- encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
- encoded_storedkey);
- encoded_storedkey[encoded_len] = '\0';
-
- /* And same for ServerKey */
- scram_ClientOrServerKey(password, saltbuf, SCRAM_SALT_LEN, iterations,
- SCRAM_SERVER_KEY_NAME, keybuf);
-
- encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
- encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
- encoded_serverkey);
- encoded_serverkey[encoded_len] = '\0';
-
- result = psprintf("SCRAM-SHA-256$%d:%s$%s:%s", iterations, encoded_salt,
- encoded_storedkey, encoded_serverkey);
-
- if (prep_password)
- pfree(prep_password);
- pfree(encoded_salt);
- pfree(encoded_storedkey);
- pfree(encoded_serverkey);
-
- return result;
-}
-
-/*
* Verify a plaintext password against a SCRAM verifier. This is used when
* performing plaintext password authentication for a user that has a SCRAM
* verifier stored in pg_authid.
@@ -576,7 +509,7 @@ parse_scram_verifier(const char *verifier, int *iterations, char **salt,
goto invalid_verifier;
/* Parse the fields */
- if (strcmp(scheme_str, "SCRAM-SHA-256") != 0)
+ if (strcmp(scheme_str, SCRAM_VERIFIER_PREFIX) != 0)
goto invalid_verifier;
errno = 0;
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index d0030f2b6d..397b559604 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,9 +20,11 @@
#include "catalog/pg_authid.h"
#include "common/md5.h"
+#include "common/scram-common.h"
#include "libpq/crypt.h"
#include "libpq/scram.h"
#include "miscadmin.h"
+#include "utils/backend_random.h"
#include "utils/builtins.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
@@ -156,8 +158,23 @@ encrypt_password(PasswordType target_type, const char *role,
switch (guessed_type)
{
case PASSWORD_TYPE_PLAINTEXT:
- return scram_build_verifier(role, password, 0);
-
+ {
+ char salt[SCRAM_SALT_LEN];
+
+ if (!pg_backend_random(salt, SCRAM_SALT_LEN))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INTERNAL_ERROR),
+ errmsg("could not generate random salt")));
+ return NULL;
+ }
+
+ encrypted_password = palloc(SCRAM_VERIFIER_LEN + 1);
+ if (!scram_build_verifier(role, password, salt, 0,
+ encrypted_password))
+ elog(ERROR, "password encryption failed");
+ return encrypted_password;
+ }
case PASSWORD_TYPE_MD5:
/*
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index df9f0eaa90..4b730a3311 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -23,6 +23,7 @@
#include <netinet/in.h>
#include <arpa/inet.h>
+#include "common/saslprep.h"
#include "common/scram-common.h"
#define HMAC_IPAD 0x36
@@ -165,3 +166,80 @@ scram_ClientOrServerKey(const char *password,
scram_HMAC_update(&ctx, keystr, strlen(keystr));
scram_HMAC_final(result, &ctx);
}
+
+/*
+ * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
+ *
+ * If iterations is 0, default number of iterations is used. The result is
+ * stored in "verifier" that caller is responsible to allocate a buffer of
+ * size SCRAM_VERIFIER_LEN. Returns true if the verifier has been generated,
+ * false otherwise. It is important for this routine to do no memory
+ * allocations (SASLprep is an exception to that as the prepared password's
+ * length is estimated locally). The salt defined by the caller needs to be a
+ * buffer pre-filled with random data of length SCRAM_SALT_LEN.
+ */
+bool
+scram_build_verifier(const char *username, const char *password,
+ const char *salt, int iterations, char *verifier)
+{
+ uint8 keybuf[SCRAM_KEY_LEN + 1];
+ char intbuf[12];
+ char *p;
+ char *prep_password = NULL;
+ pg_saslprep_rc rc;
+
+ /*
+ * Normalize the password with SASLprep. If that doesn't work, because
+ * the password isn't valid UTF-8 or contains prohibited characters, just
+ * proceed with the original password. (See comments at top of
+ * auth-scram.c.)
+ */
+ rc = pg_saslprep(password, &prep_password);
+ if (rc == SASLPREP_SUCCESS)
+ password = (const char *) prep_password;
+
+ if (iterations <= 0)
+ iterations = SCRAM_ITERATIONS_DEFAULT;
+
+ /* Fill in the data of the verifier */
+ p = verifier;
+ memcpy(p, SCRAM_VERIFIER_PREFIX, strlen(SCRAM_VERIFIER_PREFIX));
+ p += strlen(SCRAM_VERIFIER_PREFIX);
+ *p++ = '$';
+
+ /* iterations */
+ sprintf(intbuf, "%d", iterations);
+ memcpy(p, intbuf, strlen(intbuf));
+ p += strlen(intbuf);
+ *p++ = ':';
+
+ /* salt */
+ (void) pg_b64_encode(salt, SCRAM_SALT_LEN, p);
+ p += pg_b64_enc_len(SCRAM_SALT_LEN);
+ *p++ = '$';
+
+ /* Calculate StoredKey, and encode it in base64 */
+ scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN,
+ iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
+ scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
+ (void) pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, p);
+ p += pg_b64_enc_len(SCRAM_KEY_LEN) - 1;
+ *p++ = ':';
+
+ /* And same for ServerKey */
+ scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations,
+ SCRAM_SERVER_KEY_NAME, keybuf);
+ (void) pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, p);
+ p += pg_b64_enc_len(SCRAM_KEY_LEN) - 1;
+ *p++ = '\0';
+
+#ifndef FRONTEND
+ if (prep_password)
+ pfree(prep_password);
+#else
+ if (prep_password)
+ free(prep_password);
+#endif
+
+ return true;
+}
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index 6740069eee..36249a83f3 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -13,6 +13,7 @@
#ifndef SCRAM_COMMON_H
#define SCRAM_COMMON_H
+#include "common/base64.h"
#include "common/sha2.h"
/* Length of SCRAM keys (client and server) */
@@ -38,6 +39,22 @@
#define SCRAM_SERVER_KEY_NAME "Server Key"
#define SCRAM_CLIENT_KEY_NAME "Client Key"
+#define SCRAM_VERIFIER_PREFIX "SCRAM-SHA-256"
+
+/*
+ * Length of a SCRAM verifier, which is made of the following five fields
+ * separated by a colon:
+ * - prefix "SCRAM-SHA-256", made of 13 characters.
+ * - 4 colon separators.
+ * - 32-bit number of interations, up to 11 characters.
+ * - base64-encoded salt of length SCRAM_SALT_LEN
+ * - base64-encoded stored key of length SCRAM_KEY_LEN
+ * - base64-encoded server key of length SCRAM_KEY_LEN
+ */
+#define SCRAM_VERIFIER_LEN (strlen(SCRAM_VERIFIER_PREFIX) + 4 + 11 + \
+ pg_b64_enc_len(SCRAM_SALT_LEN) + \
+ pg_b64_enc_len(SCRAM_KEY_LEN) * 2)
+
/*
* Context data for HMAC used in SCRAM authentication.
*/
@@ -55,5 +72,8 @@ extern void scram_H(const uint8 *str, int len, uint8 *result);
extern void scram_ClientOrServerKey(const char *password, const char *salt,
int saltlen, int iterations,
const char *keystr, uint8 *result);
+extern bool scram_build_verifier(const char *username, const char *password,
+ const char *salt, int iterations,
+ char *verifier);
#endif /* SCRAM_COMMON_H */
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index e373f0c07e..803fc4ef7a 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -26,10 +26,7 @@ extern void *pg_be_scram_init(const char *username, const char *shadow_pass);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
-/* Routines to handle and check SCRAM-SHA-256 verifier */
-extern char *scram_build_verifier(const char *username,
- const char *password,
- int iterations);
+/* Routines to check SCRAM-SHA-256 verifier */
extern bool is_scram_verifier(const char *verifier);
extern bool scram_verify_plain_password(const char *username,
const char *password, const char *verifier);
--
2.12.2
0002-Refactor-frontend-side-random-number-generation.patchapplication/octet-stream; name=0002-Refactor-frontend-side-random-number-generation.patchDownload
From 78068e010bac158bea0e5550e7b2bd3e08dec137 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 7 Apr 2017 10:24:44 +0900
Subject: [PATCH 2/4] Refactor frontend-side random number generation
pg_frontend_random() is moved into its own file in libpq/ to give other
portions of the libpq code the ability to generate random numbers. This
will be used for the SCRAM verifier generation from clients.
---
src/interfaces/libpq/Makefile | 2 +-
src/interfaces/libpq/fe-auth-scram.c | 53 -----------------------
src/interfaces/libpq/libpq-int.h | 1 +
src/interfaces/libpq/libpq-random.c | 84 ++++++++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-random.h | 17 ++++++++
src/tools/msvc/Install.pm | 2 +-
6 files changed, 104 insertions(+), 55 deletions(-)
create mode 100644 src/interfaces/libpq/libpq-random.c
create mode 100644 src/interfaces/libpq/libpq-random.h
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 87f22d242f..eb6777ef2b 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -33,7 +33,7 @@ LIBS := $(LIBS:-lpgport=)
# OBJS from this file.
OBJS= fe-auth.o fe-auth-scram.o fe-connect.o fe-exec.o fe-misc.o fe-print.o fe-lobj.o \
fe-protocol2.o fe-protocol3.o pqexpbuffer.o fe-secure.o \
- libpq-events.o
+ libpq-events.o libpq-random.o
# libpgport C files we always use
OBJS += chklocale.o inet_net_ntop.o noblock.o pgstrcasecmp.o pqsignal.o \
thread.o
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index c56e91e0e0..b453c6cc21 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -74,7 +74,6 @@ static bool verify_server_proof(fe_scram_state *state);
static void calculate_client_proof(fe_scram_state *state,
const char *client_final_message_without_proof,
uint8 *result);
-static bool pg_frontend_random(char *dst, int len);
/*
* Initialize SCRAM exchange status.
@@ -609,55 +608,3 @@ verify_server_proof(fe_scram_state *state)
return true;
}
-
-/*
- * Random number generator.
- */
-static bool
-pg_frontend_random(char *dst, int len)
-{
-#ifdef HAVE_STRONG_RANDOM
- return pg_strong_random(dst, len);
-#else
- int i;
- char *end = dst + len;
-
- static unsigned short seed[3];
- static int mypid = 0;
-
- pglock_thread();
-
- if (mypid != getpid())
- {
- struct timeval now;
-
- gettimeofday(&now, NULL);
-
- seed[0] = now.tv_sec ^ getpid();
- seed[1] = (unsigned short) (now.tv_usec);
- seed[2] = (unsigned short) (now.tv_usec >> 16);
- }
-
- for (i = 0; dst < end; i++)
- {
- uint32 r;
- int j;
-
- /*
- * pg_jrand48 returns a 32-bit integer. Fill the next 4 bytes from
- * it.
- */
- r = (uint32) pg_jrand48(seed);
-
- for (j = 0; j < 4 && dst < end; j++)
- {
- *(dst++) = (char) (r & 0xFF);
- r >>= 8;
- }
- }
-
- pgunlock_thread();
-
- return true;
-#endif
-}
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 34d049262f..65aa51cfc1 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -22,6 +22,7 @@
/* We assume libpq-fe.h has already been included. */
#include "libpq-events.h"
+#include "libpq-random.h"
#include <time.h>
#ifndef WIN32
diff --git a/src/interfaces/libpq/libpq-random.c b/src/interfaces/libpq/libpq-random.c
new file mode 100644
index 0000000000..5e788dc821
--- /dev/null
+++ b/src/interfaces/libpq/libpq-random.c
@@ -0,0 +1,84 @@
+/*-------------------------------------------------------------------------
+ *
+ * libpq-random.c
+ * Frontend random number generation routine for libpq.
+ *
+ * pg_frontend_random() function fills a buffer with random bytes. Normally,
+ * it is just a thin wrapper around pg_strong_random(), but when compiled
+ * with --disable-strong-random, there is a built-in implementation.
+ *
+ * The built-in implementation uses the standard erand48 algorithm, with
+ * a seed calculated using the process ID and a timestamp.
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/interfaces/libpq/libpq-random.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+/* These are needed for getpid(), in the fallback implementation */
+#ifndef HAVE_STRONG_RANDOM
+#include <sys/types.h>
+#include <unistd.h>
+#endif
+
+/*
+ * Random number generator.
+ */
+bool
+pg_frontend_random(char *dst, int len)
+{
+#ifdef HAVE_STRONG_RANDOM
+ return pg_strong_random(dst, len);
+#else
+ int i;
+ char *end = dst + len;
+
+ static unsigned short seed[3];
+ static int mypid = 0;
+
+ pglock_thread();
+
+ if (mypid != getpid())
+ {
+ struct timeval now;
+
+ gettimeofday(&now, NULL);
+
+ seed[0] = now.tv_sec ^ getpid();
+ seed[1] = (unsigned short) (now.tv_usec);
+ seed[2] = (unsigned short) (now.tv_usec >> 16);
+ }
+
+ for (i = 0; dst < end; i++)
+ {
+ uint32 r;
+ int j;
+
+ /*
+ * pg_jrand48 returns a 32-bit integer. Fill the next 4 bytes from
+ * it.
+ */
+ r = (uint32) pg_jrand48(seed);
+
+ for (j = 0; j < 4 && dst < end; j++)
+ {
+ *(dst++) = (char) (r & 0xFF);
+ r >>= 8;
+ }
+ }
+
+ pgunlock_thread();
+
+ return true;
+#endif
+}
diff --git a/src/interfaces/libpq/libpq-random.h b/src/interfaces/libpq/libpq-random.h
new file mode 100644
index 0000000000..054998b8d9
--- /dev/null
+++ b/src/interfaces/libpq/libpq-random.h
@@ -0,0 +1,17 @@
+/*-------------------------------------------------------------------------
+ *
+ * frontend_random.h
+ * Declarations for frontend random number generation
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ *
+ * src/interfaces/libpq/libpq-random.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef LIBPQ_RANDOM_H
+#define LIBPQ_RANDOM_H
+
+extern bool pg_frontend_random(char *dst, int len);
+
+#endif /* LIBPQ_RANDOM_H */
diff --git a/src/tools/msvc/Install.pm b/src/tools/msvc/Install.pm
index 35ad5b8a44..4acd6592ad 100644
--- a/src/tools/msvc/Install.pm
+++ b/src/tools/msvc/Install.pm
@@ -601,7 +601,7 @@ sub CopyIncludeFiles
CopyFiles(
'Libpq internal headers',
$target . '/include/internal/',
- 'src/interfaces/libpq/', 'libpq-int.h', 'pqexpbuffer.h');
+ 'src/interfaces/libpq/', 'libpq-int.h', 'libpq-random.h', 'pqexpbuffer.h');
CopyFiles(
'Internal headers',
--
2.12.2
0003-Extend-PQencryptPassword-with-a-hashing-method.patchapplication/octet-stream; name=0003-Extend-PQencryptPassword-with-a-hashing-method.patchDownload
From d19217ea67fbe27a4c5f5d693f0e09ddc83aa69a Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 7 Apr 2017 10:39:27 +0900
Subject: [PATCH 3/4] Extend PQencryptPassword with a hashing method
This extra argument can use the following values when hashing the
password:
- scram, for SCRAM-SHA-256 hashing.
- md5, for MD5 hashing.
- plain, for cleartext.
---
doc/src/sgml/libpq.sgml | 6 ++++-
src/bin/psql/command.c | 2 +-
src/bin/scripts/createuser.c | 3 ++-
src/interfaces/libpq/fe-auth.c | 54 ++++++++++++++++++++++++++++++++---------
src/interfaces/libpq/libpq-fe.h | 3 ++-
5 files changed, 52 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 4bc5bf3192..fc1aa4b5e5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -5887,7 +5887,8 @@ void PQconninfoFree(PQconninfoOption *connOptions);
<para>
Prepares the encrypted form of a <productname>PostgreSQL</> password.
<synopsis>
-char * PQencryptPassword(const char *passwd, const char *user);
+char * PQencryptPassword(const char *passwd, const char *user,
+ const char *method);
</synopsis>
This function is intended to be used by client applications that
wish to send commands like <literal>ALTER USER joe PASSWORD
@@ -5901,6 +5902,9 @@ char * PQencryptPassword(const char *passwd, const char *user);
memory. The caller can assume the string doesn't contain any
special characters that would require escaping. Use
<function>PQfreemem</> to free the result when done with it.
+ The encryption method of the password can be specified as
+ <literal>md5</> for hashing with MD5, <literal>scram</> for
+ hashing with SCRAM-SHA-256 and <literal>plain</> for cleartext.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 859ded71f6..ccea7a5fd0 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1878,7 +1878,7 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
else
user = PQuser(pset.db);
- encrypted_password = PQencryptPassword(pw1, user);
+ encrypted_password = PQencryptPassword(pw1, user, "md5");
if (!encrypted_password)
{
diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c
index 3d74797a8f..5af263e34a 100644
--- a/src/bin/scripts/createuser.c
+++ b/src/bin/scripts/createuser.c
@@ -275,7 +275,8 @@ main(int argc, char *argv[])
char *encrypted_password;
encrypted_password = PQencryptPassword(newpassword,
- newuser);
+ newuser,
+ "md5");
if (!encrypted_password)
{
fprintf(stderr, _("Password encryption failed.\n"));
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index d81ee4f944..cdd46a216b 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -39,6 +39,7 @@
#endif
#include "common/md5.h"
+#include "common/scram-common.h"
#include "libpq-fe.h"
#include "libpq/scram.h"
#include "fe-auth.h"
@@ -1087,27 +1088,56 @@ pg_fe_getauthname(PQExpBuffer errorMessage)
* be dependent on low-level details like whether the encryption is MD5
* or something else.
*
- * Arguments are the cleartext password, and the SQL name of the user it
- * is for.
+ * Arguments are the cleartext password, the SQL name of the user it
+ * is for, and the name of password hashing method:
+ * - "scram", to hash password using SCRAM-SHA-256.
+ * - "md5", to hash password using MD5.
+ * - "plain", to get a cleartext value of password.
*
- * Return value is a malloc'd string, or NULL if out-of-memory. The client
- * may assume the string doesn't contain any special characters that would
- * require escaping.
+ * Return value is a malloc'd string, or NULL if out-of-memory or in
+ * the event of an error. The client may assume the string doesn't
+ * contain any special characters that would require escaping.
*/
char *
-PQencryptPassword(const char *passwd, const char *user)
+PQencryptPassword(const char *passwd, const char *user, const char *method)
{
char *crypt_pwd;
- crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
- if (!crypt_pwd)
- return NULL;
+ if (strcmp(method, "md5") == 0)
+ {
+ crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
+ if (!crypt_pwd)
+ return NULL;
- if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ {
+ free(crypt_pwd);
+ return NULL;
+ }
+ }
+ else if (strcmp(method, "scram") == 0)
{
- free(crypt_pwd);
- return NULL;
+ char salt[SCRAM_SALT_LEN];
+
+ crypt_pwd = malloc(SCRAM_VERIFIER_LEN + 1);
+ if (!crypt_pwd)
+ return NULL;
+
+ if (!pg_frontend_random(salt, SCRAM_SALT_LEN))
+ return NULL;
+
+ if (!scram_build_verifier(user, passwd, salt, 0, crypt_pwd))
+ {
+ free(crypt_pwd);
+ return NULL;
+ }
}
+ else if (strcmp(method, "plain") == 0)
+ {
+ crypt_pwd = strdup(passwd);
+ }
+ else
+ return NULL;
return crypt_pwd;
}
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 635af5b50e..c312dd0152 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -596,7 +596,8 @@ extern int PQenv2encoding(void);
/* === in fe-auth.c === */
-extern char *PQencryptPassword(const char *passwd, const char *user);
+extern char *PQencryptPassword(const char *passwd, const char *user,
+ const char *method);
/* === in encnames.c === */
--
2.12.2
0004-Extend-psql-s-password-and-createuser-to-handle-SCRA.patchapplication/octet-stream; name=0004-Extend-psql-s-password-and-createuser-to-handle-SCRA.patchDownload
From 12667b9848de6905478dae84581e4691ade5fe32 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Sat, 25 Mar 2017 14:07:16 +0900
Subject: [PATCH 4/4] Extend psql's \password and createuser to handle SCRAM
verifier creation
Depending on the version of PostgreSQL those utilities are connected to,
generate MD5 verifiers when connecting to a server older than 10, and
MD5 otherwise.
---
doc/src/sgml/ref/createuser.sgml | 6 ++++--
doc/src/sgml/ref/psql-ref.sgml | 4 +++-
src/bin/psql/command.c | 9 ++++++++-
src/bin/scripts/createuser.c | 16 +++++++++++++---
4 files changed, 28 insertions(+), 7 deletions(-)
diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml
index 4332008c68..4454591d79 100644
--- a/doc/src/sgml/ref/createuser.sgml
+++ b/doc/src/sgml/ref/createuser.sgml
@@ -125,7 +125,9 @@ PostgreSQL documentation
<listitem>
<para>
Encrypts the user's password stored in the database. If not
- specified, the default password behavior is used.
+ specified, the default password behavior is used. The password
+ is hashed using SCRAM-SHA-256 when connecting to a version of
+ <productname>PostgreSQL</> newer than 10, and MD5 otherwise.
</para>
</listitem>
</varlistentry>
@@ -477,7 +479,7 @@ PostgreSQL documentation
<prompt>$ </prompt><userinput>createuser -P -s -e joe</userinput>
<computeroutput>Enter password for new role: </computeroutput><userinput>xyzzy</userinput>
<computeroutput>Enter it again: </computeroutput><userinput>xyzzy</userinput>
-<computeroutput>CREATE ROLE joe PASSWORD 'md5b5f5ba1a423792b526f799ae4eb3d59e' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;</computeroutput>
+<computeroutput>CREATE ROLE joe PASSWORD 'scram-sha-256:aPheg4yCAFhStg==:4096:TOHLPF8w+NzqtcxD2oz9w5wPISutHLOXWMKoe4HCvuo=:lTG0OiB/ZH5/hsfUqnndRwfMziY5j5C6FS8IAIwL2nA=' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;</computeroutput>
</screen>
In the above example, the new password isn't actually echoed when typed,
but we show what was typed for clarity. As you see, the password is
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 3b86612862..749221aa76 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2380,7 +2380,9 @@ lo_import 152801
user). This command prompts for the new password, encrypts it, and
sends it to the server as an <command>ALTER ROLE</> command. This
makes sure that the new password does not appear in cleartext in the
- command history, the server log, or elsewhere.
+ command history, the server log, or elsewhere. The password is hashed
+ with SCRAM-SHA-256 when connecting to <productname>PostgreSQL</> 10
+ and newer versions, and with MD5 otherwise.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index ccea7a5fd0..4d575657d4 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1878,7 +1878,14 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
else
user = PQuser(pset.db);
- encrypted_password = PQencryptPassword(pw1, user, "md5");
+ /*
+ * Hash password using SCRAM-SHA-256 when connecting to servers
+ * newer than Postgres 10, and hash with MD5 otherwise.
+ */
+ if (pset.sversion < 100000)
+ encrypted_password = PQencryptPassword(pw1, user, "md5");
+ else
+ encrypted_password = PQencryptPassword(pw1, user, "scram");
if (!encrypted_password)
{
diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c
index 5af263e34a..0107497e23 100644
--- a/src/bin/scripts/createuser.c
+++ b/src/bin/scripts/createuser.c
@@ -274,9 +274,19 @@ main(int argc, char *argv[])
{
char *encrypted_password;
- encrypted_password = PQencryptPassword(newpassword,
- newuser,
- "md5");
+ /*
+ * Hash password using SCRAM-SHA-256 when connecting to servers
+ * newer than Postgres 10, and hash with MD5 otherwise.
+ */
+ if (PQserverVersion(conn) < 100000)
+ encrypted_password = PQencryptPassword(newpassword,
+ newuser,
+ "md5");
+ else
+ encrypted_password = PQencryptPassword(newpassword,
+ newuser,
+ "scram");
+
if (!encrypted_password)
{
fprintf(stderr, _("Password encryption failed.\n"));
--
2.12.2
On 04/22/2017 01:20 AM, Michael Paquier wrote:
On Sat, Apr 22, 2017 at 5:04 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
I'll continue reviewing the rest of the patch on Monday, but one glaring
issue is that we cannot add an argument to the existing libpq
PQencryptPassword() function, because that's part of the public API. It
would break all applications that use PQencryptPassword().What we need to do is to add a new function. What should we call that? We
don't have a tradition of using "Ex" or such suffix to mark extended
versions of existing functions. PQencryptPasswordWithMethod(user, pass,
method) ?Do we really want to add a new function or have a hard failure? Any
application calling PQencryptPassword may trap itself silently if the
server uses scram as hba key or if the default is switched to that,
from this point of view extending the function makes sense as well.
Yeah, there is that. But we simply cannot change the signature of an
existing function. It would not only produce compile-time errors when
building old applications, which would arguably be a good thing, but it
would also cause old binaries to segfault when dynamically linked with
the new libpq.
I think it's clear that we should have a new function that takes the
algorithm as argument. But there are open decisions on what the old
PQencryptPassword() function should do, and also what the new function
should do by default, if you don't specify an algorithm:
A) Have PQencryptPassword() return an md5 hash.
B) Have PQencryptPassword() return a SCRAM verifier
C) Have PQencryptPassword() return a SCRAM verifier if connected to a
v10 server, and an md5 hash otherwise. This is tricky, because
PQencryptPassword doesn't take a PGconn argument. It could behave like
PQescapeString() does, and choose md5/scram depending on the server
version of the last connection that was established.
For the new function, it's probably best to pass a PGconn argument. That
way we can use the connection to determine the default, and it seems to
be a good idea for future-proofing too. And an extra "options" argument
might be good, while we're at it, to e.g. specify the number of
iterations for SCRAM. So all in all, I propose the documentation for
these functions to be (I chose option C from above for this):
----
char *
PQencryptPasswordConn(PGconn *conn,
const char *passwd,
const char *user,
const char *method,
const char *options)
[this paragraph is the same as current PQencryptPassword()]
This function is intended to be used by client applications that wish to
send commands like ALTER ROLE joe PASSWORD 'pwd'. It is good practice to
not send the original cleartext password in such a command, because it
might be exposed in command logs, activity displays and so on. Instead,
use this function to convert the password to encrypted form before it is
sent.
[end of unchanged part]
This function may execute internal queries to the server to determine
appropriate defaults, using the given connection object. The call can
therefore fail if the connection is busy executing another query, or the
current transaction is aborted.
The return value is a string allocated by malloc, or NULL if out of
memory or other error. On error, a suitable message is stored in the
'conn' object. The caller can assume the string doesn't contain any
special characters that would require escaping. Use PQfreemem to free
the result when done with it.
The function arguments are:
conn
Connection object for the database where the password is to be changed.
passwd
The new password
user
Name of the role whose password is to be changed
method
Name of the password encryption method to use. Currently supported
methods are "md5" or "scram-sha-256". If method is NULL, the default for
the current database is used. [i.e. this looks at password_encryption]
options
Options specific to the encryption method, or NULL to use the
defaults. (This argument is for future expansion, there are currently no
options, and you should always pass NULL.)
char *
PQencryptPassword(const char *passwd, const char *user)
PQencryptPassword is an older, deprecated version of
PQencryptPasswodConn. The difference is that PQencryptPassword does not
require a connection object. The encryption method will be chosen
depending on the server version of the last established connection, and
built-in default options.
----
Thoughts? Unless someone has better ideas or objections, I'll go
implement that.
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Apr 25, 2017 at 11:26 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
algorithm as argument. But there are open decisions on what the old
PQencryptPassword() function should do, and also what the new function
should do by default, if you don't specify an algorithm:A) Have PQencryptPassword() return an md5 hash.
B) Have PQencryptPassword() return a SCRAM verifier
C) Have PQencryptPassword() return a SCRAM verifier if connected to a v10
server, and an md5 hash otherwise. This is tricky, because PQencryptPassword
doesn't take a PGconn argument. It could behave like PQescapeString() does,
and choose md5/scram depending on the server version of the last connection
that was established.
I vote for A - leave PQencryptPassword() as-is, and deprecate it.
Tell people to use the new function going forward.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas <robertmhaas@gmail.com> writes:
On Tue, Apr 25, 2017 at 11:26 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
A) Have PQencryptPassword() return an md5 hash.
B) Have PQencryptPassword() return a SCRAM verifier
C) Have PQencryptPassword() return a SCRAM verifier if connected to a v10
server, and an md5 hash otherwise. This is tricky, because PQencryptPassword
doesn't take a PGconn argument. It could behave like PQescapeString() does,
and choose md5/scram depending on the server version of the last connection
that was established.
I vote for A - leave PQencryptPassword() as-is, and deprecate it.
Tell people to use the new function going forward.
+1. I never much liked that magic behavior of PQescapeString, and don't
think we should replicate it elsewhere, so I definitely don't like (C).
And I don't think we can do (B) because that will break the functionality
altogether when talking to an older server. That leaves (A) plus invent
a new function.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Apr 25, 2017 at 8:29 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Tue, Apr 25, 2017 at 11:26 AM, Heikki Linnakangas <hlinnaka@iki.fi>
wrote:
A) Have PQencryptPassword() return an md5 hash.
B) Have PQencryptPassword() return a SCRAM verifier
C) Have PQencryptPassword() return a SCRAM verifier if connected to a
v10
server, and an md5 hash otherwise. This is tricky, because
PQencryptPassword
doesn't take a PGconn argument. It could behave like PQescapeString()
does,
and choose md5/scram depending on the server version of the last
connection
that was established.
I vote for A - leave PQencryptPassword() as-is, and deprecate it.
Tell people to use the new function going forward.+1. I never much liked that magic behavior of PQescapeString, and don't
think we should replicate it elsewhere, so I definitely don't like (C).
And I don't think we can do (B) because that will break the functionality
altogether when talking to an older server. That leaves (A) plus invent
a new function.
+1.
--
Magnus Hagander
Me: https://www.hagander.net/ <http://www.hagander.net/>
Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/>
On Wed, Apr 26, 2017 at 12:26 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
Yeah, there is that. But we simply cannot change the signature of an
existing function. It would not only produce compile-time errors when
building old applications, which would arguably be a good thing, but it
would also cause old binaries to segfault when dynamically linked with the
new libpq.
Sure.
I think it's clear that we should have a new function that takes the
algorithm as argument. But there are open decisions on what the old
PQencryptPassword() function should do, and also what the new function
should do by default, if you don't specify an algorithm:A) Have PQencryptPassword() return an md5 hash.
B) Have PQencryptPassword() return a SCRAM verifier
C) Have PQencryptPassword() return a SCRAM verifier if connected to a v10
server, and an md5 hash otherwise. This is tricky, because PQencryptPassword
doesn't take a PGconn argument. It could behave like PQescapeString() does,
and choose md5/scram depending on the server version of the last connection
that was established.
Like the rest I vote for A, and document it as deprecated.
----
char *
PQencryptPasswordConn(PGconn *conn,
const char *passwd,
const char *user,
const char *method,
const char *options)[...]
Good for me, I was first thinking as well about having "default" as
keyword, NULL is fine as well.
Could it be possible to name the new function as PQhashPassword
instead of PQencryptPassword? From the previous threads we agreed that
encryption makes no sense in this context.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 04/25/2017 06:26 PM, Heikki Linnakangas wrote:
Thoughts? Unless someone has better ideas or objections, I'll go
implement that.
This is what I came up with in the end. Some highlights and differences
vs the plan I posted earlier:
* If algorithm is not given explicitly, PQencryptPasswordConn() queries
"SHOW password_encryption", and uses that. That is documented, and it is
also documented that it will *not* issue queries, and hence will not
block, if the algorithm is given explicitly. That's an important
property for some applications. If you want the default behavior without
blocking, query "SHOW password_encryption" yourself, and pass the result
as the 'algorithm'.
* In the previous draft, I envisioned an algorithm-specific 'options'
argument. I left it out, on second thoughts (more on that below).
* The old PQencryptPassword() function is unchanged, and always uses
md5. Per public opinion.
We talked about the alternative where PQencryptPasswordConn() would not
look at password_encryption, but would always use the strongest possible
algorithm supported by the server. That would avoid querying the server.
But then I started thinking how this would work, if we make the number
of iterations in the SCRAM verifier configurable in the future. The
client would not know the desired number of iterations based only on the
server version, it would need to query the server, and we would be back
to square one. We could add an "options" argument to
PQencryptPasswordConn() that the application could use to pass that
information, but documenting how to fetch that information, if you don't
want PQencryptPasswordConn() to block, gets tedious, and puts a lot of
burden to applications. That is why I left out the "options" argument,
after all.
I'm now thinking that if we add password hashing options like the
iteration count in the future, they will be tacked on to
password_encryption. For example, "password_encryption='scram-sha-256,
iterations=10000". That way, "password_encryption" will always contain
enough information to construct password verifiers.
What will happen to existing applications using PQencryptPasswordConn()
if a new password_encryption algorithm (or options) is added in the
future? With this scheme, the application doesn't need to know what
algorithms exist. An application can pass algorithm=NULL, to use the
server default, or do "show password_encryption" and pass that, for the
non-blocking behavior. If you use an older libpq against a new server,
so that libpq doesn't know about the algorithm used by the server, you
get an error.
For reviewer convenience, I put up the patched docs at
http://hlinnaka.iki.fi/temp/scram-wip-docs/libpq-misc.html#libpq-pqencryptpasswordconn.
Thoughts? Am I missing anything?
As an alternative, I considered making password_encryption GUC_REPORT,
so that libpq would always know it without having to issue a query. But
it feels wrong to send that option to the client on every connection,
when it's only needed in the rare case that you use
PQencryptPasswordConn(). And if we added new settings like the iteration
count in the future, those would need to be GUC_REPORT too.
- Heikki
Attachments:
0001-Move-scram_build_verifier-to-src-common.patchapplication/x-download; name=0001-Move-scram_build_verifier-to-src-common.patchDownload
From a656e0ab2aaed016ed5244c7bb79752acf5b71b7 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Wed, 26 Apr 2017 12:53:49 +0300
Subject: [PATCH 1/3] Move scram_build_verifier to src/common.
This is in preparation for the next patch to add support for building
SCRAM verifiers to libpq.
Michael Paquier and me
Discussion: https://www.postgresql.org/message-id/CAMkU%3D1wfBgFPbfAMYZQE78p%3DVhZX7nN86aWkp0QcCp%3D%2BKxZ%3Dbg%40mail.gmail.com
---
src/backend/libpq/auth-scram.c | 62 ++++++++------------------------------
src/backend/libpq/crypt.c | 2 +-
src/common/scram-common.c | 63 +++++++++++++++++++++++++++++++++++++++
src/include/common/scram-common.h | 7 +++--
src/include/libpq/scram.h | 4 +--
5 files changed, 83 insertions(+), 55 deletions(-)
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 16bea446e3..56b8935847 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -207,7 +207,7 @@ pg_be_scram_init(const char *username, const char *shadow_pass)
*/
char *verifier;
- verifier = scram_build_verifier(username, shadow_pass, 0);
+ verifier = pg_be_scram_build_verifier(shadow_pass);
(void) parse_scram_verifier(verifier, &state->iterations, &state->salt,
state->StoredKey, state->ServerKey);
@@ -387,21 +387,14 @@ pg_be_scram_exchange(void *opaq, char *input, int inputlen,
/*
* Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
*
- * If iterations is 0, default number of iterations is used. The result is
- * palloc'd, so caller is responsible for freeing it.
+ * The result is palloc'd, so caller is responsible for freeing it.
*/
char *
-scram_build_verifier(const char *username, const char *password,
- int iterations)
+pg_be_scram_build_verifier(const char *password)
{
char *prep_password = NULL;
pg_saslprep_rc rc;
- char saltbuf[SCRAM_SALT_LEN];
- uint8 keybuf[SCRAM_KEY_LEN];
- char *encoded_salt;
- char *encoded_storedkey;
- char *encoded_serverkey;
- int encoded_len;
+ char saltbuf[SCRAM_DEFAULT_SALT_LEN];
char *result;
/*
@@ -413,11 +406,8 @@ scram_build_verifier(const char *username, const char *password,
if (rc == SASLPREP_SUCCESS)
password = (const char *) prep_password;
- if (iterations <= 0)
- iterations = SCRAM_ITERATIONS_DEFAULT;
-
- /* Generate salt, and encode it in base64 */
- if (!pg_backend_random(saltbuf, SCRAM_SALT_LEN))
+ /* Generate salt */
+ if (!pg_backend_random(saltbuf, SCRAM_DEFAULT_SALT_LEN))
{
ereport(LOG,
(errcode(ERRCODE_INTERNAL_ERROR),
@@ -425,37 +415,11 @@ scram_build_verifier(const char *username, const char *password,
return NULL;
}
- encoded_salt = palloc(pg_b64_enc_len(SCRAM_SALT_LEN) + 1);
- encoded_len = pg_b64_encode(saltbuf, SCRAM_SALT_LEN, encoded_salt);
- encoded_salt[encoded_len] = '\0';
-
- /* Calculate StoredKey, and encode it in base64 */
- scram_ClientOrServerKey(password, saltbuf, SCRAM_SALT_LEN,
- iterations, SCRAM_CLIENT_KEY_NAME, keybuf);
- scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
-
- encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
- encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
- encoded_storedkey);
- encoded_storedkey[encoded_len] = '\0';
-
- /* And same for ServerKey */
- scram_ClientOrServerKey(password, saltbuf, SCRAM_SALT_LEN, iterations,
- SCRAM_SERVER_KEY_NAME, keybuf);
-
- encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
- encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
- encoded_serverkey);
- encoded_serverkey[encoded_len] = '\0';
-
- result = psprintf("SCRAM-SHA-256$%d:%s$%s:%s", iterations, encoded_salt,
- encoded_storedkey, encoded_serverkey);
+ result = scram_build_verifier(saltbuf, SCRAM_DEFAULT_SALT_LEN,
+ SCRAM_DEFAULT_ITERATIONS, password);
if (prep_password)
pfree(prep_password);
- pfree(encoded_salt);
- pfree(encoded_storedkey);
- pfree(encoded_serverkey);
return result;
}
@@ -630,12 +594,12 @@ mock_scram_verifier(const char *username, int *iterations, char **salt,
/* Generate deterministic salt */
raw_salt = scram_MockSalt(username);
- encoded_salt = (char *) palloc(pg_b64_enc_len(SCRAM_SALT_LEN) + 1);
- encoded_len = pg_b64_encode(raw_salt, SCRAM_SALT_LEN, encoded_salt);
+ encoded_salt = (char *) palloc(pg_b64_enc_len(SCRAM_DEFAULT_SALT_LEN) + 1);
+ encoded_len = pg_b64_encode(raw_salt, SCRAM_DEFAULT_SALT_LEN, encoded_salt);
encoded_salt[encoded_len] = '\0';
*salt = encoded_salt;
- *iterations = SCRAM_ITERATIONS_DEFAULT;
+ *iterations = SCRAM_DEFAULT_ITERATIONS;
/* StoredKey and ServerKey are not used in a doomed authentication */
memset(stored_key, 0, SCRAM_KEY_LEN);
@@ -1192,9 +1156,9 @@ scram_MockSalt(const char *username)
* Generate salt using a SHA256 hash of the username and the cluster's
* mock authentication nonce. (This works as long as the salt length is
* not larger the SHA256 digest length. If the salt is smaller, the caller
- * will just ignore the extra data))
+ * will just ignore the extra data.)
*/
- StaticAssertStmt(PG_SHA256_DIGEST_LENGTH >= SCRAM_SALT_LEN,
+ StaticAssertStmt(PG_SHA256_DIGEST_LENGTH >= SCRAM_DEFAULT_SALT_LEN,
"salt length greater than SHA256 digest length");
pg_sha256_init(&ctx);
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index d0030f2b6d..9fe79b4894 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -156,7 +156,7 @@ encrypt_password(PasswordType target_type, const char *role,
switch (guessed_type)
{
case PASSWORD_TYPE_PLAINTEXT:
- return scram_build_verifier(role, password, 0);
+ return pg_be_scram_build_verifier(password);
case PASSWORD_TYPE_MD5:
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index df9f0eaa90..38a23de73e 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -23,6 +23,7 @@
#include <netinet/in.h>
#include <arpa/inet.h>
+#include "common/base64.h"
#include "common/scram-common.h"
#define HMAC_IPAD 0x36
@@ -165,3 +166,65 @@ scram_ClientOrServerKey(const char *password,
scram_HMAC_update(&ctx, keystr, strlen(keystr));
scram_HMAC_final(result, &ctx);
}
+
+
+/*
+ * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
+ *
+ * The password should already have been processed with SASLprep, if necessary!
+ *
+ * If iterations is 0, default number of iterations is used. The result is
+ * palloc'd or malloc'd, so caller is responsible for freeing it.
+ */
+char *
+scram_build_verifier(const char *salt, int saltlen, int iterations,
+ const char *password)
+{
+ uint8 storedkeybuf[SCRAM_KEY_LEN];
+ uint8 serverkeybuf[SCRAM_KEY_LEN];
+ char *result;
+ char *p;
+ int maxlen;
+
+ if (iterations <= 0)
+ iterations = SCRAM_DEFAULT_ITERATIONS;
+
+ /* Calculate StoredKey and ServerKey */
+ scram_ClientOrServerKey(password, salt, saltlen,
+ iterations, SCRAM_CLIENT_KEY_NAME, storedkeybuf);
+ scram_H(storedkeybuf, SCRAM_KEY_LEN, storedkeybuf);
+
+ scram_ClientOrServerKey(password, salt, saltlen, iterations,
+ SCRAM_SERVER_KEY_NAME, serverkeybuf);
+
+ /*
+ * The format is:
+ * SCRAM-SHA-256$<iteration count>:<salt>$<StoredKey>:<ServerKey>
+ */
+ maxlen = strlen("SCRAM-SHA-256") + 1
+ + 10 + 1 /* iteration count */
+ + pg_b64_enc_len(saltlen) + 1 /* Base64-encoded salt */
+ + pg_b64_enc_len(SCRAM_KEY_LEN) + 1 /* Base64-encoded StoredKey */
+ + pg_b64_enc_len(SCRAM_KEY_LEN) + 1; /* Base64-encoded ServerKey */
+
+#ifdef FRONTEND
+ result = malloc(maxlen);
+ if (!result)
+ return NULL;
+#else
+ result = palloc(maxlen);
+#endif
+
+ p = result + sprintf(result, "SCRAM-SHA-256$%d:", iterations);
+
+ p += pg_b64_encode(salt, saltlen, p);
+ *(p++) = '$';
+ p += pg_b64_encode((char *) storedkeybuf, SCRAM_KEY_LEN, p);
+ *(p++) = ':';
+ p += pg_b64_encode((char *) serverkeybuf, SCRAM_KEY_LEN, p);
+ *(p++) = '\0';
+
+ Assert(p - result <= maxlen);
+
+ return result;
+}
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index 6740069eee..cc69aafade 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -29,10 +29,10 @@
#define SCRAM_RAW_NONCE_LEN 10
/* length of salt when generating new verifiers */
-#define SCRAM_SALT_LEN 10
+#define SCRAM_DEFAULT_SALT_LEN 10
/* default number of iterations when generating verifier */
-#define SCRAM_ITERATIONS_DEFAULT 4096
+#define SCRAM_DEFAULT_ITERATIONS 4096
/* Base name of keys used for proof generation */
#define SCRAM_SERVER_KEY_NAME "Server Key"
@@ -56,4 +56,7 @@ extern void scram_ClientOrServerKey(const char *password, const char *salt,
int saltlen, int iterations,
const char *keystr, uint8 *result);
+extern char *scram_build_verifier(const char *salt, int saltlen, int iterations,
+ const char *password);
+
#endif /* SCRAM_COMMON_H */
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index e373f0c07e..060b8af69e 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -27,9 +27,7 @@ extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
/* Routines to handle and check SCRAM-SHA-256 verifier */
-extern char *scram_build_verifier(const char *username,
- const char *password,
- int iterations);
+extern char *pg_be_scram_build_verifier(const char *password);
extern bool is_scram_verifier(const char *verifier);
extern bool scram_verify_plain_password(const char *username,
const char *password, const char *verifier);
--
2.11.0
0002-Add-PQencryptPasswordConn-function-to-libpq.patchapplication/x-download; name=0002-Add-PQencryptPasswordConn-function-to-libpq.patchDownload
From eea84c48129a613e6385facb76d5a19978906b7a Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Wed, 26 Apr 2017 12:55:35 +0300
Subject: [PATCH 2/3] Add PQencryptPasswordConn function to libpq.
The new function supports creating SCRAM verifiers, in addition to md5
hashes. The algorithm is chosen based on password_encryption, by default.
Michael Paquier and me
Discussion: https://www.postgresql.org/message-id/CAMkU%3D1wfBgFPbfAMYZQE78p%3DVhZX7nN86aWkp0QcCp%3D%2BKxZ%3Dbg%40mail.gmail.com
---
doc/src/sgml/libpq.sgml | 66 +++++++++++++++---
src/interfaces/libpq/exports.txt | 1 +
src/interfaces/libpq/fe-auth-scram.c | 32 +++++++++
src/interfaces/libpq/fe-auth.c | 125 +++++++++++++++++++++++++++++++----
src/interfaces/libpq/fe-auth.h | 1 +
src/interfaces/libpq/libpq-fe.h | 1 +
6 files changed, 203 insertions(+), 23 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 4bc5bf3192..2228386eea 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -5875,11 +5875,11 @@ void PQconninfoFree(PQconninfoOption *connOptions);
</listitem>
</varlistentry>
- <varlistentry id="libpq-pqencryptpassword">
+ <varlistentry id="libpq-pqencryptpasswordconn">
<term>
- <function>PQencryptPassword</function>
+ <function>PQencryptPasswordConn</function>
<indexterm>
- <primary>PQencryptPassword</primary>
+ <primary>PQencryptPasswordConn</primary>
</indexterm>
</term>
@@ -5887,20 +5887,64 @@ void PQconninfoFree(PQconninfoOption *connOptions);
<para>
Prepares the encrypted form of a <productname>PostgreSQL</> password.
<synopsis>
-char * PQencryptPassword(const char *passwd, const char *user);
+char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm);
</synopsis>
This function is intended to be used by client applications that
wish to send commands like <literal>ALTER USER joe PASSWORD
'pwd'</>. It is good practice not to send the original cleartext
password in such a command, because it might be exposed in command
logs, activity displays, and so on. Instead, use this function to
- convert the password to encrypted form before it is sent. The
- arguments are the cleartext password, and the SQL name of the user
- it is for. The return value is a string allocated by
- <function>malloc</function>, or <symbol>NULL</symbol> if out of
- memory. The caller can assume the string doesn't contain any
- special characters that would require escaping. Use
- <function>PQfreemem</> to free the result when done with it.
+ convert the password to encrypted form before it is sent.
+ </para>
+
+ <para>
+ The <parameter>passwd</> and <parameter>user</> arguments
+ are the cleartext password, and the SQL name of the user it is for.
+ <parameter>algorithm</> specifies the encryption algorithm
+ to use to encrypt the password. Currently supported algorithms are
+ <literal>md5</> and <literal>scram-sha-256</>. <literal>scram-sha-256</>
+ was introduced in <productname>PostgreSQL</> version 10, and will
+ not work correctly with older server versions. If <parameter>algorithm</>
+ is <symbol>NULL</>, this function will query the server for the current
+ value of the <xref linkend="guc-password-encryption"> setting. That can
+ block, and will fail if the current transaction is aborted, or if the
+ connection is busy executing another query. If you wish to use the
+ default algorithm for the server but want to avoid blocking, query
+ <varname>password_encryption</> yourself before calling
+ <function>PQencryptPasswordConn</>, and pass that value as the
+ <parameter>algorithm</>.
+ </para>
+
+ <para>
+ The return value is a string allocated by <function>malloc</>.
+ The caller can assume the string doesn't contain any special characters
+ that would require escaping. Use <function>PQfreemem</> to free the
+ result when done with it. On error, returns <symbol>NULL</>, and
+ a suitable message is stored in the connection object.
+ </para>
+
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="libpq-pqencryptpassword">
+ <term>
+ <function>PQencryptPassword</function>
+ <indexterm>
+ <primary>PQencryptPassword</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ Prepares the md5-encrypted form of a <productname>PostgreSQL</> password.
+ <synopsis>
+char *PQencryptPassword(const char *passwd, const char *user);
+ </synopsis>
+ <function>PQencryptPassword</> is an older, deprecated version of
+ <function>PQencryptPasswodConn</>. The difference is that
+ <function>PQencryptPassword</> does not
+ require a connection object, and <literal>md5</> is always used as the
+ encryption algorithm.
</para>
</listitem>
</varlistentry>
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 21dd772ca9..d6a38d0df8 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -171,3 +171,4 @@ PQsslAttributeNames 168
PQsslAttribute 169
PQsetErrorContextVisibility 170
PQresultVerboseErrorMessage 171
+PQencryptPasswordConn 172
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index c56e91e0e0..6910c1e6db 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -610,6 +610,38 @@ verify_server_proof(fe_scram_state *state)
return true;
}
+char *
+pg_fe_scram_build_verifier(const char *password)
+{
+ char *prep_password = NULL;
+ pg_saslprep_rc rc;
+ char saltbuf[SCRAM_DEFAULT_SALT_LEN];
+ char *result;
+
+ /*
+ * Normalize the password with SASLprep. If that doesn't work, because
+ * the password isn't valid UTF-8 or contains prohibited characters, just
+ * proceed with the original password. (See comments at top of file.)
+ */
+ rc = pg_saslprep(password, &prep_password);
+ if (rc == SASLPREP_OOM)
+ return NULL;
+ if (rc == SASLPREP_SUCCESS)
+ password = (const char *) prep_password;
+
+ /* Generate salt */
+ if (!pg_frontend_random(saltbuf, SCRAM_DEFAULT_SALT_LEN))
+ return NULL;
+
+ result = scram_build_verifier(saltbuf, SCRAM_DEFAULT_SALT_LEN,
+ SCRAM_DEFAULT_ITERATIONS, password);
+
+ if (prep_password)
+ free(prep_password);
+
+ return result;
+}
+
/*
* Random number generator.
*/
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index d81ee4f944..517854a305 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -1077,7 +1077,33 @@ pg_fe_getauthname(PQExpBuffer errorMessage)
/*
- * PQencryptPassword -- exported routine to encrypt a password
+ * PQencryptPassword -- exported routine to encrypt a password with MD5
+ *
+ * This function is equivalent to calling PQencryptPasswordConn with
+ * "md5" as the encryption method, except that this doesn't require
+ * a connection object. This function is deprecated, use
+ * PQencryptPasswordConn instead.
+ */
+char *
+PQencryptPassword(const char *passwd, const char *user)
+{
+ char *crypt_pwd;
+
+ crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
+ if (!crypt_pwd)
+ return NULL;
+
+ if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ {
+ free(crypt_pwd);
+ return NULL;
+ }
+
+ return crypt_pwd;
+}
+
+/*
+ * PQencryptPasswordConn -- exported routine to encrypt a password
*
* This is intended to be used by client applications that wish to send
* commands like ALTER USER joe PASSWORD 'pwd'. The password need not
@@ -1087,27 +1113,102 @@ pg_fe_getauthname(PQExpBuffer errorMessage)
* be dependent on low-level details like whether the encryption is MD5
* or something else.
*
- * Arguments are the cleartext password, and the SQL name of the user it
- * is for.
+ * Arguments are a connection object, the cleartext password, the SQL
+ * name of the user it is for, and a string indicating the algorithm to
+ * use for encrypting the password. If algorithm is NULL, this queries
+ * the server for the current 'password_encryption' value. If you wish
+ * to avoid that, e.g. to avoid blocking, you can execute
+ * 'show password_encryption' yourself before calling this function, and pass
+ * it as the algorithm.
*
- * Return value is a malloc'd string, or NULL if out-of-memory. The client
- * may assume the string doesn't contain any special characters that would
- * require escaping.
+ * Return value is a malloc'd string. The client may assume the string
+ * doesn't contain any special characters that would require escaping.
+ * On error, an error message is stored in the connection object, and
+ * returns NULL.
*/
char *
-PQencryptPassword(const char *passwd, const char *user)
+PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user,
+ const char *algorithm)
{
- char *crypt_pwd;
+#define MAX_ALGORITHM_NAME_LEN 50
+ char algobuf[MAX_ALGORITHM_NAME_LEN + 1];
+ char *crypt_pwd = NULL;
- crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
- if (!crypt_pwd)
+ if (!conn)
return NULL;
- if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ /* If no algorithm was given, ask the server. */
+ if (algorithm == NULL)
{
- free(crypt_pwd);
+ PGresult *res;
+ char *val;
+
+ res = PQexec(conn, "show password_encryption");
+ if (res == NULL)
+ {
+ /* PQexec() should've set conn->errorMessage already */
+ return NULL;
+ }
+ if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ {
+ /* PQexec() should've set conn->errorMessage already */
+ PQclear(res);
+ return NULL;
+ }
+ if (PQntuples(res) != 1 || PQnfields(res) != 1)
+ {
+ PQclear(res);
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("unexpected shape of result set returned for SHOW\n"));
+ return NULL;
+ }
+ val = PQgetvalue(res, 0, 0);
+
+ if (strlen(val) > MAX_ALGORITHM_NAME_LEN)
+ {
+ PQclear(res);
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("password_encryption value too long\n"));
+ return NULL;
+ }
+ strcpy(algobuf, val);
+ PQclear(res);
+
+ algorithm = algobuf;
+ }
+
+ /* Ok, now we know what algorithm to use */
+
+ if (strcmp(algorithm, "scram-sha-256") == 0)
+ {
+ crypt_pwd = pg_fe_scram_build_verifier(passwd);
+ }
+ else if (strcmp(algorithm, "md5") == 0)
+ {
+ crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
+ if (crypt_pwd)
+ {
+ if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+ {
+ free(crypt_pwd);
+ crypt_pwd = NULL;
+ }
+ }
+ }
+ else if (strcmp(algorithm, "plain") == 0)
+ {
+ crypt_pwd = strdup(passwd);
+ }
+ else
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("unknown password encryption algorithm\n"));
return NULL;
}
+ if (!crypt_pwd)
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("out of memory\n"));
+
return crypt_pwd;
}
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index a5c739f01a..9f4c2a50d8 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -28,5 +28,6 @@ extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
bool *done, bool *success, PQExpBuffer errorMessage);
+extern char *pg_fe_scram_build_verifier(const char *password);
#endif /* FE_AUTH_H */
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 635af5b50e..c1497f00b6 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -597,6 +597,7 @@ extern int PQenv2encoding(void);
/* === in fe-auth.c === */
extern char *PQencryptPassword(const char *passwd, const char *user);
+extern char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *method);
/* === in encnames.c === */
--
2.11.0
0003-Use-new-PQencryptPasswordConn-function-in-psql-and-c.patchapplication/x-download; name=0003-Use-new-PQencryptPasswordConn-function-in-psql-and-c.patchDownload
From a335389bbe31fdf365250fdbdf1f5d01940dad86 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Wed, 26 Apr 2017 12:57:12 +0300
Subject: [PATCH 3/3] Use new PQencryptPasswordConn function in psql and
createuser
This fixes the issue reported by Jeff Janes, that there was previously
no way to create a SCRAM verifier with "\password".
Michael Paquier and me
Discussion: https://www.postgresql.org/message-id/CAMkU%3D1wfBgFPbfAMYZQE78p%3DVhZX7nN86aWkp0QcCp%3D%2BKxZ%3Dbg%40mail.gmail.com
---
src/bin/psql/command.c | 2 +-
src/bin/scripts/createuser.c | 6 ++++--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 859ded71f6..51bd8f9cc9 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1878,7 +1878,7 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
else
user = PQuser(pset.db);
- encrypted_password = PQencryptPassword(pw1, user);
+ encrypted_password = PQencryptPasswordConn(pset.db, pw1, user, NULL);
if (!encrypted_password)
{
diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c
index 3d74797a8f..087a5ca05f 100644
--- a/src/bin/scripts/createuser.c
+++ b/src/bin/scripts/createuser.c
@@ -274,8 +274,10 @@ main(int argc, char *argv[])
{
char *encrypted_password;
- encrypted_password = PQencryptPassword(newpassword,
- newuser);
+ encrypted_password = PQencryptPasswordConn(conn,
+ newpassword,
+ newuser,
+ NULL);
if (!encrypted_password)
{
fprintf(stderr, _("Password encryption failed.\n"));
--
2.11.0
On Wed, Apr 26, 2017 at 6:22 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 04/25/2017 06:26 PM, Heikki Linnakangas wrote:
Thoughts? Unless someone has better ideas or objections, I'll go
implement that.This is what I came up with in the end. Some highlights and differences vs
the plan I posted earlier:* If algorithm is not given explicitly, PQencryptPasswordConn() queries
"SHOW password_encryption", and uses that. That is documented, and it is
also documented that it will *not* issue queries, and hence will not block,
if the algorithm is given explicitly. That's an important property for some
applications. If you want the default behavior without blocking, query "SHOW
password_encryption" yourself, and pass the result as the 'algorithm'.
TBH, I'd just require the user to specify the algorithm explicitly.
Having it run SHOW on the server seems wonky. It introduces a bunch
of failure modes for ... no real benefit, I think.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas <robertmhaas@gmail.com> writes:
On Wed, Apr 26, 2017 at 6:22 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
* If algorithm is not given explicitly, PQencryptPasswordConn() queries
"SHOW password_encryption", and uses that. That is documented, and it is
also documented that it will *not* issue queries, and hence will not block,
if the algorithm is given explicitly. That's an important property for some
applications. If you want the default behavior without blocking, query "SHOW
password_encryption" yourself, and pass the result as the 'algorithm'.
TBH, I'd just require the user to specify the algorithm explicitly.
Having it run SHOW on the server seems wonky. It introduces a bunch
of failure modes for ... no real benefit, I think.
Yeah. Blocking is the least of your worries --- what about being in
a failed transaction, for instance?
However, it's not entirely true that there's no real benefit. If the
client app has to specify the algorithm then client code will need
extension every time we add another algorithm. Maybe that's going to
happen so seldom that it's not a big deal, but it would be nice to
avoid that.
Would it be worth making password_encryption be GUC_REPORT so that
it could be guaranteed available, without a server transaction,
from any valid connection? I'm generally resistant to adding
GUC_REPORT flags, but maybe this is a time for an exception.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Apr 26, 2017 at 12:57 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Would it be worth making password_encryption be GUC_REPORT so that
it could be guaranteed available, without a server transaction,
from any valid connection? I'm generally resistant to adding
GUC_REPORT flags, but maybe this is a time for an exception.
Well, as Heikki just wrote a few messages upthread:
---
As an alternative, I considered making password_encryption GUC_REPORT,
so that libpq would always know it without having to issue a query.
But it feels wrong to send that option to the client on every
connection, when it's only needed in the rare case that you use
PQencryptPasswordConn(). And if we added new settings like the
iteration count in the future, those would need to be GUC_REPORT too.
---
I think those are both good points.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Apr 26, 2017 at 7:22 PM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
We talked about the alternative where PQencryptPasswordConn() would not look
at password_encryption, but would always use the strongest possible
algorithm supported by the server. That would avoid querying the server. But
then I started thinking how this would work, if we make the number of
iterations in the SCRAM verifier configurable in the future. The client
would not know the desired number of iterations based only on the server
version, it would need to query the server, and we would be back to square
one. We could add an "options" argument to PQencryptPasswordConn() that the
application could use to pass that information, but documenting how to fetch
that information, if you don't want PQencryptPasswordConn() to block, gets
tedious, and puts a lot of burden to applications. That is why I left out
the "options" argument, after all.
Fine for me.
I'm now thinking that if we add password hashing options like the iteration
count in the future, they will be tacked on to password_encryption. For
example, "password_encryption='scram-sha-256, iterations=10000". That way,
"password_encryption" will always contain enough information to construct
password verifiers.
That's possible as well, adding more GUCs for sub-options of a hashing
algorithm is wrong.
As an alternative, I considered making password_encryption GUC_REPORT, so
that libpq would always know it without having to issue a query. But it
feels wrong to send that option to the client on every connection, when it's
only needed in the rare case that you use PQencryptPasswordConn(). And if we
added new settings like the iteration count in the future, those would need
to be GUC_REPORT too.
Agreed, PQencryptPassword is not that widely used..
Here are some comments.
+ /*
+ * Normalize the password with SASLprep. If that doesn't work, because
+ * the password isn't valid UTF-8 or contains prohibited
characters, just
+ * proceed with the original password. (See comments at top of file.)
+ */
+ rc = pg_saslprep(password, &prep_password);
This comment is not true, comments are at the top of auth-scram.c.
+ * The password should already have been processed with SASLprep, if necessary!
+ *
+ * If iterations is 0, default number of iterations is used. The result is
+ * palloc'd or malloc'd, so caller is responsible for freeing it.
+ */
+char *
+scram_build_verifier(const char *salt, int saltlen, int iterations,
+ const char *password)
+{
+ uint8 storedkeybuf[SCRAM_KEY_LEN];
+ uint8 serverkeybuf[SCRAM_KEY_LEN];
+ char *result;
+ char *p;
+ int maxlen;
I think that it is a mistake to move SASLprep out of
scram_build_verifier, because pre-processing the password is not
necessary, it is normally mandatory. The BE/FE versions that you are
adding also duplicate the calls to pg_saslprep().
Using "encrypt" instead of "hash" in the function name :(
+ else if (strcmp(algorithm, "plain") == 0)
+ {
+ crypt_pwd = strdup(passwd);
+ }
This is not documented, and users should be warned about using it as well.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Apr 25, 2017 at 8:56 PM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 04/22/2017 01:20 AM, Michael Paquier wrote:
On Sat, Apr 22, 2017 at 5:04 AM, Heikki Linnakangas <hlinnaka@iki.fi>
wrote:I'll continue reviewing the rest of the patch on Monday, but one glaring
issue is that we cannot add an argument to the existing libpq
PQencryptPassword() function, because that's part of the public API. It
would break all applications that use PQencryptPassword().What we need to do is to add a new function. What should we call that? We
don't have a tradition of using "Ex" or such suffix to mark extended
versions of existing functions. PQencryptPasswordWithMethod(user, pass,
method) ?Do we really want to add a new function or have a hard failure? Any
application calling PQencryptPassword may trap itself silently if the
server uses scram as hba key or if the default is switched to that,
from this point of view extending the function makes sense as well.Yeah, there is that. But we simply cannot change the signature of an
existing function. It would not only produce compile-time errors when
building old applications, which would arguably be a good thing, but it
would also cause old binaries to segfault when dynamically linked with the
new libpq.I think it's clear that we should have a new function that takes the
algorithm as argument. But there are open decisions on what the old
PQencryptPassword() function should do, and also what the new function
should do by default, if you don't specify an algorithm:A) Have PQencryptPassword() return an md5 hash.
B) Have PQencryptPassword() return a SCRAM verifier
C) Have PQencryptPassword() return a SCRAM verifier if connected to a v10
server, and an md5 hash otherwise. This is tricky, because
PQencryptPassword doesn't take a PGconn argument. It could behave like
PQescapeString() does, and choose md5/scram depending on the server version
of the last connection that was established.For the new function, it's probably best to pass a PGconn argument. That
way we can use the connection to determine the default, and it seems to be
a good idea for future-proofing too. And an extra "options" argument might
be good, while we're at it, to e.g. specify the number of iterations for
SCRAM. So all in all, I propose the documentation for these functions to be
(I chose option C from above for this):----
char *
PQencryptPasswordConn(PGconn *conn,
const char *passwd,
const char *user,
const char *method,
const char *options)
I was just thinking:
- Do we need to provide the method here?
We have connection object itself, it can decide from the type of
connection, which method to be used.
-- Thanks, Ashesh
Show quoted text
[this paragraph is the same as current PQencryptPassword()]
This function is intended to be used by client applications that wish to
send commands like ALTER ROLE joe PASSWORD 'pwd'. It is good practice to
not send the original cleartext password in such a command, because it
might be exposed in command logs, activity displays and so on. Instead, use
this function to convert the password to encrypted form before it is sent.
[end of unchanged part]This function may execute internal queries to the server to determine
appropriate defaults, using the given connection object. The call can
therefore fail if the connection is busy executing another query, or the
current transaction is aborted.The return value is a string allocated by malloc, or NULL if out of memory
or other error. On error, a suitable message is stored in the 'conn'
object. The caller can assume the string doesn't contain any special
characters that would require escaping. Use PQfreemem to free the result
when done with it.The function arguments are:
conn
Connection object for the database where the password is to be changed.passwd
The new passworduser
Name of the role whose password is to be changedmethod
Name of the password encryption method to use. Currently supported
methods are "md5" or "scram-sha-256". If method is NULL, the default for
the current database is used. [i.e. this looks at password_encryption]options
Options specific to the encryption method, or NULL to use the defaults.
(This argument is for future expansion, there are currently no options, and
you should always pass NULL.)char *
PQencryptPassword(const char *passwd, const char *user)PQencryptPassword is an older, deprecated version of PQencryptPasswodConn.
The difference is that PQencryptPassword does not require a connection
object. The encryption method will be chosen depending on the server
version of the last established connection, and built-in default options.----
Thoughts? Unless someone has better ideas or objections, I'll go implement
that.- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Apr 27, 2017 at 1:25 PM, Ashesh Vashi
<ashesh.vashi@enterprisedb.com> wrote:
- Do we need to provide the method here?
We have connection object itself, it can decide from the type of connection,
which method to be used.
Providing the method is not mandatory. If you look upthread... If the
caller of this routine specified method = NULL, then the value of
password_encryption on the server is queried automatically and that
will be the method used to hash the password.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Apr 27, 2017 at 9:57 AM, Michael Paquier <michael.paquier@gmail.com>
wrote:
On Thu, Apr 27, 2017 at 1:25 PM, Ashesh Vashi
<ashesh.vashi@enterprisedb.com> wrote:- Do we need to provide the method here?
We have connection object itself, it can decide from the type ofconnection,
which method to be used.
Providing the method is not mandatory. If you look upthread... If the
caller of this routine specified method = NULL, then the value of
password_encryption on the server is queried automatically and that
will be the method used to hash the password.
I missed that. Sorry.
Thanks.
--Thanks, Ashesh
Show quoted text
--
Michael
On 04/26/2017 07:57 PM, Tom Lane wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Wed, Apr 26, 2017 at 6:22 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
* If algorithm is not given explicitly, PQencryptPasswordConn() queries
"SHOW password_encryption", and uses that. That is documented, and it is
also documented that it will *not* issue queries, and hence will not block,
if the algorithm is given explicitly. That's an important property for some
applications. If you want the default behavior without blocking, query "SHOW
password_encryption" yourself, and pass the result as the 'algorithm'.TBH, I'd just require the user to specify the algorithm explicitly.
Having it run SHOW on the server seems wonky. It introduces a bunch
of failure modes for ... no real benefit, I think.Yeah. Blocking is the least of your worries --- what about being in
a failed transaction, for instance?
Well, the "ALTER USER" command that you will surely run next, using the
same connection, would fail anyway. I don't think running a query is a
problem, as long as it's documented, and there's a documented way to
avoid it.
You could argue, that since we need to document how to avoid the query
and the blocking, we might as well always require the application to run
the "show password_encryption" query before calling
PQencryptPasswordConn(). But I'd like to offer the convenience for the
majority of applications that don't mind blocking.
However, it's not entirely true that there's no real benefit. If the
client app has to specify the algorithm then client code will need
extension every time we add another algorithm. Maybe that's going to
happen so seldom that it's not a big deal, but it would be nice to
avoid that.
Yeah, I'd like to be prepared. Hopefully we don't need to add another
algorithm any time soon, but maybe we will, or maybe we want to add an
option for the SCRAM iteration count, for example.
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Apr 21, 2017 at 11:04:14PM +0300, Heikki Linnakangas wrote:
I'll continue reviewing the rest of the patch on Monday, but [...]
This PostgreSQL 10 open item is past due for your status update. Kindly send
a status update within 24 hours, and include a date for your subsequent status
update. Moreover, this open item has been in progress for 17 days, materially
longer than the 1-2 week RMT non-intervention period. Refer to the policy on
open item ownership:
/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 04/28/2017 07:49 AM, Noah Misch wrote:
On Fri, Apr 21, 2017 at 11:04:14PM +0300, Heikki Linnakangas wrote:
I'll continue reviewing the rest of the patch on Monday, but [...]
This PostgreSQL 10 open item is past due for your status update. Kindly send
a status update within 24 hours, and include a date for your subsequent status
update. Moreover, this open item has been in progress for 17 days, materially
longer than the 1-2 week RMT non-intervention period. Refer to the policy on
open item ownership:
/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com
I just pushed some little cleanups that caught my eye while working on
this. I need to rebase the patch set I sent earlier, and fix the little
things that Michael pointed out, but that shouldn't take long. I plan to
push a fix for this on Tuesday (Monday is a national holiday here).
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Apr 27, 2017 at 3:22 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
You could argue, that since we need to document how to avoid the query and
the blocking, we might as well always require the application to run the
"show password_encryption" query before calling PQencryptPasswordConn(). But
I'd like to offer the convenience for the majority of applications that
don't mind blocking.
I still think that's borrowing trouble. It just seems like too
critical of a thing to have a default -- if the convenience logic gets
it wrong and encrypts the password in a manner not intended by the
user, that could (a) amount to a security vulnerability or (b) lock
you out of your account. If you ask your significant other "where do
you want to go to dinner?" and can't get a clear answer out of them
after some period of time, it's probably OK to assume they don't care
that much and you can just pick something. If you ask the
commander-in-chief "which country should we fire the missiles at?" and
you don't get a clear and unambiguous answer, just picking something
is not a very good idea.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 05/01/2017 07:04 PM, Robert Haas wrote:
On Thu, Apr 27, 2017 at 3:22 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
You could argue, that since we need to document how to avoid the query and
the blocking, we might as well always require the application to run the
"show password_encryption" query before calling PQencryptPasswordConn(). But
I'd like to offer the convenience for the majority of applications that
don't mind blocking.I still think that's borrowing trouble. It just seems like too
critical of a thing to have a default -- if the convenience logic gets
it wrong and encrypts the password in a manner not intended by the
user, that could (a) amount to a security vulnerability or (b) lock
you out of your account.
That's true for a lot of things. The logic isn't complicated; it runs
"SHOW password_encryption", and uses the value of that as the algorithm.
There's going to be a default, one way or another. The default is going
to come from password_encryption, or it's going to be a hard-coded value
or logic based on server-version in PQencryptPasswordConn(). Or it's
going to be a hard-coded value or logic implemented in every application
that uses PQencryptPasswordConn(). I think looking at
password_encryption makes the most sense. The application is not in a
good position to make the decision, and forcing the end-user to choose
every time they change a password is too onerous.
If you ask your significant other "where do
you want to go to dinner?" and can't get a clear answer out of them
after some period of time, it's probably OK to assume they don't care
that much and you can just pick something. If you ask the
commander-in-chief "which country should we fire the missiles at?" and
you don't get a clear and unambiguous answer, just picking something
is not a very good idea.
I don't understand the analogy. If the application explicitly passes an
algorithm, we use that. If the application passes NULL, it means "you
decide, based on the documented rules". If the application passes
"mumble-mumble", you get an error. If the "SHOW password_encryption"
query fails or libpq doesn't understand the result, you also get an error.
What do you think we should do here, then? Make password_encryption
GUC_REPORT? Hard-code an algorithm in every application? Remove the
convenience logic from PQencryptionPasswordConn(), and document that for
a sensible default, the application should first run "SHOW
password_encryption", and use the result of that as the algorithm? Or go
with the current patch?
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, May 2, 2017 at 3:42 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
There's going to be a default, one way or another. The default is going to
come from password_encryption, or it's going to be a hard-coded value or
logic based on server-version in PQencryptPasswordConn(). Or it's going to
be a hard-coded value or logic implemented in every application that uses
PQencryptPasswordConn(). I think looking at password_encryption makes the
most sense. The application is not in a good position to make the decision,
and forcing the end-user to choose every time they change a password is too
onerous.
I think there should be no default, and the caller should have to pass
the algorithm explicitly. If they want to determine what default to
pass by running 'SHOW password_encryption', that's their choice.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 05/02/2017 07:47 PM, Robert Haas wrote:
On Tue, May 2, 2017 at 3:42 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
There's going to be a default, one way or another. The default is going to
come from password_encryption, or it's going to be a hard-coded value or
logic based on server-version in PQencryptPasswordConn(). Or it's going to
be a hard-coded value or logic implemented in every application that uses
PQencryptPasswordConn(). I think looking at password_encryption makes the
most sense. The application is not in a good position to make the decision,
and forcing the end-user to choose every time they change a password is too
onerous.I think there should be no default, and the caller should have to pass
the algorithm explicitly. If they want to determine what default to
pass by running 'SHOW password_encryption', that's their choice.
Ok, gotcha. I disagree, I think we should provide a default. Libpq is in
a better position to make a good choice than most applications.
I've committed the new PQencryptPasswordConn function, with the default
behavior of doing "show password_encryption", and the changes to use it
in psql and createuser. This closes the open issue with \password.
On 04/27/2017 07:03 AM, Michael Paquier wrote:
I think that it is a mistake to move SASLprep out of
scram_build_verifier, because pre-processing the password is not
necessary, it is normally mandatory. The BE/FE versions that you are
adding also duplicate the calls to pg_saslprep().
I played with that a little bit, but decided to keep pg_saslprep() out
of scram_build_verifier() after all. It would seem asymmetric to have
scram_build_verifier() call pg_saslprep(), but require callers of
scram_SaltedPassword() to call it. So for consistency, I think
scram_SaltedPassword() should also call pg_saslprep(). That would
complicated the scram_SaltedPassword() function, however. It would need
to report an OOM error somehow, for starters. Not an insurmountable
issue, of course, but it felt cleaner this way, after all, despite the
duplication.
Using "encrypt" instead of "hash" in the function name :(
Yeah. For better or worse, I've kept the "encrypt" nomenclature
everywhere, for consistency.
Thanks!
- Heikki
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, May 3, 2017 at 10:26 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 05/02/2017 07:47 PM, Robert Haas wrote:
On Tue, May 2, 2017 at 3:42 AM, Heikki Linnakangas <hlinnaka@iki.fi>
wrote:There's going to be a default, one way or another. The default is going
to
come from password_encryption, or it's going to be a hard-coded value or
logic based on server-version in PQencryptPasswordConn(). Or it's going
to
be a hard-coded value or logic implemented in every application that uses
PQencryptPasswordConn(). I think looking at password_encryption makes the
most sense. The application is not in a good position to make the
decision,
and forcing the end-user to choose every time they change a password is
too
onerous.I think there should be no default, and the caller should have to pass
the algorithm explicitly. If they want to determine what default to
pass by running 'SHOW password_encryption', that's their choice.Ok, gotcha. I disagree, I think we should provide a default. Libpq is in a
better position to make a good choice than most applications.I've committed the new PQencryptPasswordConn function, with the default
behavior of doing "show password_encryption", and the changes to use it in
psql and createuser. This closes the open issue with \password.
If we're basically just telling people to call SHOW manually, we might as
well do it in the default case. I think the wording you put into the docs
there is good, as it tells people exactly what happens and how to reproduce
it locally.
For the security perspective, perhaps we should have a link to the part of
the docs that discusses the different algorithms?
--
Magnus Hagander
Me: https://www.hagander.net/ <http://www.hagander.net/>
Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/>
On Wed, May 3, 2017 at 5:26 PM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:
Ok, gotcha. I disagree, I think we should provide a default. Libpq is in a
better position to make a good choice than most applications.I've committed the new PQencryptPasswordConn function, with the default
behavior of doing "show password_encryption", and the changes to use it in
psql and createuser. This closes the open issue with \password.
Well, there is always the counter argument that applications can check
for password_encryption by themselves and complete
PQencryptPasswordConn and that would be a couple of extra lines in any
applications. But honestly, people will appreciate a way to rely on
what the backend uses automatically.
On 04/27/2017 07:03 AM, Michael Paquier wrote:
I think that it is a mistake to move SASLprep out of
scram_build_verifier, because pre-processing the password is not
necessary, it is normally mandatory. The BE/FE versions that you are
adding also duplicate the calls to pg_saslprep().I played with that a little bit, but decided to keep pg_saslprep() out of
scram_build_verifier() after all. It would seem asymmetric to have
scram_build_verifier() call pg_saslprep(), but require callers of
scram_SaltedPassword() to call it. So for consistency, I think
scram_SaltedPassword() should also call pg_saslprep(). That would
complicated the scram_SaltedPassword() function, however. It would need to
report an OOM error somehow, for starters. Not an insurmountable issue, of
course, but it felt cleaner this way, after all, despite the duplication.
Okay, I won't fight on that further.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers