Skip to content

add scram-sha-256 support #1313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions lib/puppet/functions/postgresql/postgresql_password.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# frozen_string_literal: true
require 'openssl'
require 'base64'

# @summary This function returns the postgresql password hash from the clear text username / password
Puppet::Functions.create_function(:'postgresql::postgresql_password') do
Expand All @@ -8,23 +10,66 @@
# The clear text `password`
# @param sensitive
# If the Postgresql-Passwordhash should be of Datatype Sensitive[String]
# @param hash
# Set type for password hash
# @param salt
# Use a specific salt value for scram-sha-256, default is username
#
# @return
# The postgresql password hash from the clear text username / password.
dispatch :default_impl do
required_param 'Variant[String[1], Integer]', :username
required_param 'Variant[String[1], Sensitive[String[1]], Integer]', :password
optional_param 'Boolean', :sensitive
optional_param "Optional[Enum['md5', 'scram-sha-256']]", :hash
optional_param 'Optional[Variant[String[1], Integer]]', :salt
return_type 'Variant[String, Sensitive[String]]'
end

def default_impl(username, password, sensitive = false)
def default_impl(username, password, sensitive = false, hash = 'md5', salt = nil)
password = password.unwrap if password.respond_to?(:unwrap)
result_string = 'md5' + Digest::MD5.hexdigest(password.to_s + username.to_s)
pass = if hash == 'md5'
'md5' + Digest::MD5.hexdigest(password.to_s + username.to_s)
else
pg_sha256(password, (salt || username))
end
if sensitive
Puppet::Pops::Types::PSensitiveType::Sensitive.new(result_string)
Puppet::Pops::Types::PSensitiveType::Sensitive.new(pass)
else
result_string
pass
end
end

def pg_sha256(password, salt)
digest = digest_key(password, salt)
'SCRAM-SHA-256$%s:%s$%s:%s' % [
'4096',
Base64.strict_encode64(salt),
Base64.strict_encode64(client_key(digest)),
Base64.strict_encode64(server_key(digest))
]
end

def digest_key(password, salt)
OpenSSL::KDF.pbkdf2_hmac(
password,
salt: salt,
iterations: 4096,
length: 32,
hash: OpenSSL::Digest::SHA256.new
)
end

def client_key(digest_key)
hmac = OpenSSL::HMAC.new(digest_key, OpenSSL::Digest::SHA256.new)
hmac << 'Client Key'
hmac.digest
OpenSSL::Digest.new('SHA256').digest hmac.digest
end

def server_key(digest_key)
hmac = OpenSSL::HMAC.new(digest_key, OpenSSL::Digest::SHA256.new)
hmac << 'Server Key'
hmac.digest
end
end
17 changes: 13 additions & 4 deletions manifests/server/role.pp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
# @param psql_group Sets the OS group to run psql
# @param psql_path Sets path to psql command
# @param module_workdir Specifies working directory under which the psql command should be executed. May need to specify if '/tmp' is on volume mounted with noexec option.
# @param hash Specify the hash method for pg password
# @param salt Specify the salt use for the scram-sha-256 encoding password (default username)
define postgresql::server::role (
$update_password = true,
Variant[Boolean, String, Sensitive[String]] $password_hash = false,
Expand All @@ -37,6 +39,8 @@
$psql_path = $postgresql::server::psql_path,
$module_workdir = $postgresql::server::module_workdir,
Enum['present', 'absent'] $ensure = 'present',
Enum['md5', 'scram-sha-256'] $hash = 'md5',
Optional[Variant[String[1], Integer]] $salt = undef,
) {
$password_hash_unsensitive = if $password_hash =~ Sensitive[String] {
$password_hash.unwrap
Expand Down Expand Up @@ -130,14 +134,19 @@
}

if $password_hash_unsensitive and $update_password {
if($password_hash_unsensitive =~ /^md5.+/) {
if($password_hash_unsensitive =~ /^(md5|SCRAM-SHA-256).+/) {
$pwd_hash_sql = $password_hash_unsensitive
} else {
$pwd_md5 = md5("${password_hash_unsensitive}${username}")
$pwd_hash_sql = "md5${pwd_md5}"
$pwd_hash_sql = postgresql::postgresql_password(
$username,
$password_hash,
$password_hash =~ Sensitive[String],
$hash,
$salt,
)
}
postgresql_psql { "ALTER ROLE ${username} ENCRYPTED PASSWORD ****":
command => Sensitive("ALTER ROLE \"${username}\" ${password_sql}"),
command => Sensitive("ALTER ROLE \"${username}\" ENCRYPTED PASSWORD '${pwd_hash_sql}'"),
unless => Sensitive("SELECT 1 FROM pg_shadow WHERE usename = '${username}' AND passwd = '${pwd_hash_sql}'"),
sensitive => true,
}
Expand Down
18 changes: 16 additions & 2 deletions spec/spec_helper_local.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,24 @@ def param(type, title, param)
it { is_expected.not_to eq(nil) }

it {
is_expected.to run.with_params('foo', 'bar').and_return('md596948aad3fcae80c08a35c9b5958cd89')
is_expected.to run.with_params('foo', 'bar').and_return(
'md596948aad3fcae80c08a35c9b5958cd89'
)
}
it {
is_expected.to run.with_params('foo', 1234).and_return('md539a0e1b308278a8de5e007cd1f795920')
is_expected.to run.with_params('foo', 1234).and_return(
'md539a0e1b308278a8de5e007cd1f795920'
)
}
it {
is_expected.to run.with_params('foo', 'bar', nil, 'scram-sha-256').and_return(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nil parameter gives an error for me:

expected postgresql_password("foo", "bar", nil, "scram-sha-256", "salt") to have returned "SCRAM-SHA-256$4096:c2FsdA==$zOt2zFfUQMbpQf3/vRnYB33QDK/L7APOBHniLy39j/4=:DcW5Jp8Do7wYhVp1f9aT0cyhUfzIAozGcvzXZj+M3YI=" instead of raising ArgumentError('postgresql::postgresql_password' parameter 'sensitive' expects a Boolean value, got Undef)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello,

Effectively postgresql_password have a mandatory boolean on 3 positions, I don't understand why it's work on my ci (I'm a beginner with puppet functions),

That should be like you say here #1313 (comment)

irb(main):001:0> require 'openssl'
=> true
irb(main):002:0> require 'base64'
=> true
irb(main):003:0>   def pg_sha256(password, salt)
    digest = digest_key(password, salt)
    'SCRAM-SHA-256$%s:%s$%s:%s' % [
      '4096',
      Base64.strict_encode64(salt),
      Base64.strict_encode64(client_key(digest)),
      Base64.strict_encode64(server_key(digest))
    ]
  end

  def digest_key(password, salt)
    OpenSSL::KDF.pbkdf2_hmac(
      password,
      salt: salt,
      iterations: 4096,
      length: 32,
      hash: OpenSSL::Digest::SHA256.new
    )
  end

  def client_key(digest_key)
    hmac = OpenSSL::HMAC.new(digest_key, OpenSSL::Digest::SHA256.new)
    hmac << 'Client Key'
    hmac.digest
    OpenSSL::Digest.new('SHA256').digest hmac.digest
  end

  def server_key(digest_key)
    hmac = OpenSSL::HMAC.new(digest_key, OpenSSL::Digest::SHA256.new)
    hmac << 'Server Key'
    hmac.digest
  end
=> :pg_sha256
=> :digest_key
=> :client_key
=> :server_key
irb(main):036:0> pg_sha256('bar', 'foo')
=> "SCRAM-SHA-256$4096:Zm9v$ea66ynZ8cS9Ty4ZkEYicwC72StsKLSwjcXIXKMgepTk=:dJYmOU6BMCaWkQOB3lrXH9OAF3lW2n3NJ26NO7Srq7U="

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want I try to fix this on a new PR ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do. I just merged a PR which already fixed some Rubocop failures so be sure to base it on the latest commit.

'SCRAM-SHA-256$4096:YmFy$y1VOaTvvs4V3OECvMzre9FtgCZClGuBLVE6sNPsTKbs=:HwFqmSKbihSyHMqkhufOy++cWCFIoTRSg8y6YgeALzE='
Copy link
Collaborator

@ekohl ekohl Feb 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my local system I'm getting a different value:

expected postgresql_password("foo", "bar", false, "scram-sha-256") to have returned "SCRAM-SHA-256$4096:YmFy$y1VOaTvvs4V3OECvMzre9FtgCZClGuBLVE6sNPsTKbs=:HwFqmSKbihSyHMqkhufOy++cWCFIoTRSg8y6YgeALzE=" instead of "SCRAM-SHA-256$4096:Zm9v$ea66ynZ8cS9Ty4ZkEYicwC72StsKLSwjcXIXKMgepTk=:dJYmOU6BMCaWkQOB3lrXH9OAF3lW2n3NJ26NO7Srq7U="

)
}
it {
is_expected.to run.with_params('foo', 'bar', nil, 'scram-sha-256', 'salt').and_return(
'SCRAM-SHA-256$4096:c2FsdA==$zOt2zFfUQMbpQf3/vRnYB33QDK/L7APOBHniLy39j/4=:DcW5Jp8Do7wYhVp1f9aT0cyhUfzIAozGcvzXZj+M3YI='
)
}
it 'raises an error if there is only 1 argument' do
is_expected.to run.with_params('foo').and_raise_error(StandardError)
Expand Down