Commit ccae190b authored by Michael Paquier's avatar Michael Paquier

Fix detection of passwords hashed with MD5 or SCRAM-SHA-256

This commit fixes a couple of issues related to the way password
verifiers hashed with MD5 or SCRAM-SHA-256 are detected, leading to
being able to store in catalogs passwords which do not follow the
supported hash formats:
- A MD5-hashed entry was checked based on if its header uses "md5" and
if the string length matches what is expected.  Unfortunately the code
never checked if the hash only used hexadecimal characters, as reported
by Tom Lane.
- A SCRAM-hashed entry was checked based on only its header, which
should be "SCRAM-SHA-256$", but it never checked for any fields
afterwards, as reported by Jonathan Katz.

Backpatch down to v10, which is where SCRAM has been introduced, and
where password verifiers in plain format have been removed.

Author: Jonathan Katz
Reviewed-by: Tom Lane, Michael Paquier
Discussion: https://postgr.es/m/016deb6b-1f0a-8e9f-1833-a8675b170aa9@postgresql.org
Backpatch-through: 10
parent b5f58cf2
...@@ -161,8 +161,6 @@ static char *build_server_first_message(scram_state *state); ...@@ -161,8 +161,6 @@ static char *build_server_first_message(scram_state *state);
static char *build_server_final_message(scram_state *state); static char *build_server_final_message(scram_state *state);
static bool verify_client_proof(scram_state *state); static bool verify_client_proof(scram_state *state);
static bool verify_final_nonce(scram_state *state); static bool verify_final_nonce(scram_state *state);
static bool parse_scram_verifier(const char *verifier, int *iterations,
char **salt, uint8 *stored_key, uint8 *server_key);
static void mock_scram_verifier(const char *username, int *iterations, static void mock_scram_verifier(const char *username, int *iterations,
char **salt, uint8 *stored_key, uint8 *server_key); char **salt, uint8 *stored_key, uint8 *server_key);
static bool is_scram_printable(char *p); static bool is_scram_printable(char *p);
...@@ -546,7 +544,7 @@ scram_verify_plain_password(const char *username, const char *password, ...@@ -546,7 +544,7 @@ scram_verify_plain_password(const char *username, const char *password,
* *
* Returns true if the SCRAM verifier has been parsed, and false otherwise. * Returns true if the SCRAM verifier has been parsed, and false otherwise.
*/ */
static bool bool
parse_scram_verifier(const char *verifier, int *iterations, char **salt, parse_scram_verifier(const char *verifier, int *iterations, char **salt,
uint8 *stored_key, uint8 *server_key) uint8 *stored_key, uint8 *server_key)
{ {
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
#include "catalog/pg_authid.h" #include "catalog/pg_authid.h"
#include "common/md5.h" #include "common/md5.h"
#include "common/scram-common.h"
#include "libpq/crypt.h" #include "libpq/crypt.h"
#include "libpq/scram.h" #include "libpq/scram.h"
#include "miscadmin.h" #include "miscadmin.h"
...@@ -90,9 +91,17 @@ get_role_password(const char *role, char **logdetail) ...@@ -90,9 +91,17 @@ get_role_password(const char *role, char **logdetail)
PasswordType PasswordType
get_password_type(const char *shadow_pass) get_password_type(const char *shadow_pass)
{ {
if (strncmp(shadow_pass, "md5", 3) == 0 && strlen(shadow_pass) == MD5_PASSWD_LEN) char *encoded_salt;
int iterations;
uint8 stored_key[SCRAM_KEY_LEN];
uint8 server_key[SCRAM_KEY_LEN];
if (strncmp(shadow_pass, "md5", 3) == 0 &&
strlen(shadow_pass) == MD5_PASSWD_LEN &&
strspn(shadow_pass + 3, MD5_PASSWD_CHARSET) == MD5_PASSWD_LEN - 3)
return PASSWORD_TYPE_MD5; return PASSWORD_TYPE_MD5;
if (strncmp(shadow_pass, "SCRAM-SHA-256$", strlen("SCRAM-SHA-256$")) == 0) if (parse_scram_verifier(shadow_pass, &iterations, &encoded_salt,
stored_key, server_key))
return PASSWORD_TYPE_SCRAM_SHA_256; return PASSWORD_TYPE_SCRAM_SHA_256;
return PASSWORD_TYPE_PLAINTEXT; return PASSWORD_TYPE_PLAINTEXT;
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
#ifndef PG_MD5_H #ifndef PG_MD5_H
#define PG_MD5_H #define PG_MD5_H
#define MD5_PASSWD_CHARSET "0123456789abcdef"
#define MD5_PASSWD_LEN 35 #define MD5_PASSWD_LEN 35
extern bool pg_md5_hash(const void *buff, size_t len, char *hexsum); extern bool pg_md5_hash(const void *buff, size_t len, char *hexsum);
......
...@@ -29,6 +29,8 @@ extern int pg_be_scram_exchange(void *opaq, const char *input, int inputlen, ...@@ -29,6 +29,8 @@ extern int pg_be_scram_exchange(void *opaq, const char *input, int inputlen,
/* Routines to handle and check SCRAM-SHA-256 verifier */ /* Routines to handle and check SCRAM-SHA-256 verifier */
extern char *pg_be_scram_build_verifier(const char *password); extern char *pg_be_scram_build_verifier(const char *password);
extern bool parse_scram_verifier(const char *verifier, int *iterations, char **salt,
uint8 *stored_key, uint8 *server_key);
extern bool scram_verify_plain_password(const char *username, extern bool scram_verify_plain_password(const char *username,
const char *password, const char *verifier); const char *password, const char *verifier);
......
...@@ -62,6 +62,15 @@ SET password_encryption = 'scram-sha-256'; ...@@ -62,6 +62,15 @@ SET password_encryption = 'scram-sha-256';
ALTER ROLE regress_passwd4 PASSWORD 'foo'; ALTER ROLE regress_passwd4 PASSWORD 'foo';
-- already encrypted with MD5, use as it is -- already encrypted with MD5, use as it is
CREATE ROLE regress_passwd5 PASSWORD 'md5e73a4b11df52a6068f8b39f90be36023'; CREATE ROLE regress_passwd5 PASSWORD 'md5e73a4b11df52a6068f8b39f90be36023';
-- This looks like a valid SCRAM-SHA-256 verifier, but it is not
-- so it should be hashed with SCRAM-SHA-256.
CREATE ROLE regress_passwd6 PASSWORD 'SCRAM-SHA-256$1234';
-- These may look like valid MD5 verifiers, but they are not, so they
-- should be hashed with SCRAM-SHA-256.
-- trailing garbage at the end
CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
-- invalid length
CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid FROM pg_authid
WHERE rolname LIKE 'regress_passwd%' WHERE rolname LIKE 'regress_passwd%'
...@@ -73,7 +82,10 @@ SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+ ...@@ -73,7 +82,10 @@ SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+
regress_passwd3 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey> regress_passwd3 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
regress_passwd4 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey> regress_passwd4 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
regress_passwd5 | md5e73a4b11df52a6068f8b39f90be36023 regress_passwd5 | md5e73a4b11df52a6068f8b39f90be36023
(5 rows) regress_passwd6 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
regress_passwd7 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
regress_passwd8 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
(8 rows)
-- An empty password is not allowed, in any form -- An empty password is not allowed, in any form
CREATE ROLE regress_passwd_empty PASSWORD ''; CREATE ROLE regress_passwd_empty PASSWORD '';
...@@ -93,6 +105,9 @@ DROP ROLE regress_passwd2; ...@@ -93,6 +105,9 @@ DROP ROLE regress_passwd2;
DROP ROLE regress_passwd3; DROP ROLE regress_passwd3;
DROP ROLE regress_passwd4; DROP ROLE regress_passwd4;
DROP ROLE regress_passwd5; DROP ROLE regress_passwd5;
DROP ROLE regress_passwd6;
DROP ROLE regress_passwd7;
DROP ROLE regress_passwd8;
DROP ROLE regress_passwd_empty; DROP ROLE regress_passwd_empty;
-- all entries should have been removed -- all entries should have been removed
SELECT rolname, rolpassword SELECT rolname, rolpassword
......
...@@ -54,6 +54,16 @@ ALTER ROLE regress_passwd4 PASSWORD 'foo'; ...@@ -54,6 +54,16 @@ ALTER ROLE regress_passwd4 PASSWORD 'foo';
-- already encrypted with MD5, use as it is -- already encrypted with MD5, use as it is
CREATE ROLE regress_passwd5 PASSWORD 'md5e73a4b11df52a6068f8b39f90be36023'; CREATE ROLE regress_passwd5 PASSWORD 'md5e73a4b11df52a6068f8b39f90be36023';
-- This looks like a valid SCRAM-SHA-256 verifier, but it is not
-- so it should be hashed with SCRAM-SHA-256.
CREATE ROLE regress_passwd6 PASSWORD 'SCRAM-SHA-256$1234';
-- These may look like valid MD5 verifiers, but they are not, so they
-- should be hashed with SCRAM-SHA-256.
-- trailing garbage at the end
CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
-- invalid length
CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
FROM pg_authid FROM pg_authid
WHERE rolname LIKE 'regress_passwd%' WHERE rolname LIKE 'regress_passwd%'
...@@ -70,6 +80,9 @@ DROP ROLE regress_passwd2; ...@@ -70,6 +80,9 @@ DROP ROLE regress_passwd2;
DROP ROLE regress_passwd3; DROP ROLE regress_passwd3;
DROP ROLE regress_passwd4; DROP ROLE regress_passwd4;
DROP ROLE regress_passwd5; DROP ROLE regress_passwd5;
DROP ROLE regress_passwd6;
DROP ROLE regress_passwd7;
DROP ROLE regress_passwd8;
DROP ROLE regress_passwd_empty; DROP ROLE regress_passwd_empty;
-- all entries should have been removed -- all entries should have been removed
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment