Skip to content
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

VAULT-13614 Support SCRAM-SHA-256 encrypted passwords for PostgreSQL #19616

Merged
merged 56 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
da0d8ae
scram password hash support
raymonstah Mar 8, 2023
e8db5e5
provide "password_encryption" config
raymonstah Mar 17, 2023
49bc306
fmt
raymonstah Mar 17, 2023
72e8096
changelog
raymonstah Mar 17, 2023
5a54a44
add test docs
raymonstah Mar 17, 2023
e032a37
ldaputil: adds comment on available text/template functions (#19469)
austingebauer Mar 7, 2023
f2b2b0b
UI: OIDC callback bug. (#18521)
davidspek Mar 7, 2023
b55c3b2
Delete test-link-rewrites.yml (#19467)
ashleemboyer Mar 7, 2023
c27026d
Change headings to h2 (#19402)
maxiscoding28 Mar 7, 2023
88988e1
UI/vault 12818/oracle banner sll (#19019)
Monkeychip Mar 8, 2023
8185d68
UI: fix delete for SSH engine config (#19448)
hellobontempo Mar 8, 2023
72c5ce8
bug: correct sdk handling of the zero int64 value (#18729)
valli0x Mar 9, 2023
e8f8084
VAULT-14215 Fix panic for non-TLS listeners during SIGHUP (#19483)
VioletHynes Mar 9, 2023
25e359f
Fix failing TestHCPLinkConnected Test (#19474)
Mar 9, 2023
139fad6
Remove the note about Vault not supporting number Okta verify push nu…
yhyakuna Mar 10, 2023
e4957d6
Add info about gcp service account key encoding (#19496)
robmonte Mar 10, 2023
0e8ecd0
Un-hiding link to 1.13 upgrade guide (#19505)
mladlow Mar 10, 2023
0fb9b15
sdk: Fix fmt + add FieldType test (#19493)
tomhjp Mar 10, 2023
3e4bc9f
Add support for importing RSA-PSS keys into Transit (#19519)
cipherboy Mar 13, 2023
edef778
Remove .mdx extension from link (#19514)
ashleemboyer Mar 13, 2023
068c146
change mul and div functions (#19495)
rculpepper Mar 13, 2023
915c245
add comment to explain use of math/rand package in lifetime_watcher (…
Mar 14, 2023
e210cae
Fix a possible data race with rollback manager and plugin reload (#19…
fairclothjm Mar 14, 2023
aaf2824
Remove oracle banner (#19532)
Monkeychip Mar 14, 2023
f0e9dea
Ignore special HTTP fields in response validation tests (#19530)
averche Mar 14, 2023
7983f92
VAULT-12798 Correct removal behaviour when JWT is symlink (#18863)
VioletHynes Mar 14, 2023
0102e61
PKI Responses Part 4 (#18612)
AnPucel Mar 14, 2023
5d4f9b8
Glimmer Navigate Input component (#19517)
Monkeychip Mar 15, 2023
28590b8
Add Oracle Cloud auth to the Vault Agent (#19260)
F21 Mar 15, 2023
559627c
Update auto-auth docs to remove tilde for home (#19548)
VioletHynes Mar 15, 2023
fd60cdc
Add the Tokenization/Rotation persistence issue as a Known Issue (#19…
sgmiller Mar 15, 2023
9aa7fee
adding copyright header (#19555)
hghaf099 Mar 15, 2023
405f598
database/elasticsearch: upgrades plugin to v0.13.1 (#19545)
austingebauer Mar 15, 2023
4c8d062
ci: pin terraform until planning bug is resolved (#19560)
ryancragun Mar 15, 2023
a579239
Add upgrade note for Removed builtins in 1.13 (#19531)
mpalmi Mar 15, 2023
ebf5b63
comment out HCP_SCADA_ADDRESS environment variable (#19583)
Mar 16, 2023
8ddea0b
Fix remount for mounts with spaces in the name (#19585)
VioletHynes Mar 16, 2023
08231ce
[QT-523] Remove copyright/license header from raft config used in the…
Mar 16, 2023
97b68f5
VAULT-14204 Update parameter policy documentation (#19586)
miagilepner Mar 17, 2023
3d4ba60
update link policy fetch URL (#19371)
ccapurso Mar 17, 2023
882b172
UI: Glimmerize BoxRadio and AlertPopup (#19571)
kiannaquach Mar 17, 2023
76c7871
UI: Glimmerize - Colocate template and remove component file (#19569)
kiannaquach Mar 17, 2023
addc7c0
vault-12244 (#19591)
hghaf099 Mar 17, 2023
1071315
UI: Glimmerize InfoTable, PageHeader, UpgradePage, NamespaceReminder,…
kiannaquach Mar 17, 2023
6039a6c
Update KV-V2 docs to explicitly call out the secret mount path as a p…
VioletHynes Mar 17, 2023
0ad6835
UI: Glimmerize Chevron, EmptyState, FieldGroupShow, InfoTooltip, Icon…
kiannaquach Mar 17, 2023
ef51b7b
Add known issue about OCSP GET redirection responses (#19523)
stevendpclark Mar 17, 2023
cb65677
Merge branch 'main' into raymond/scram-support
raymonstah Mar 17, 2023
3ca59d1
add scram package to test_packages
raymonstah Mar 17, 2023
99e2d6e
add scram package to test_packages
raymonstah Mar 20, 2023
e0f926e
sort imports
raymonstah Mar 20, 2023
b998c23
add SCRAM library to copywrite ignore
raymonstah Mar 20, 2023
efe94fd
rename from Encrypt to Hash
raymonstah Mar 20, 2023
ef287a2
rename password_encryption to password_authentication
raymonstah Mar 20, 2023
ceaba05
rename password_encryption to password_authentication (part 2)
raymonstah Mar 20, 2023
aac9e01
rename encrypted to hash in docs
raymonstah Mar 21, 2023
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
1 change: 1 addition & 0 deletions .copywrite.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ project {
"builtin/credential/aws/pkcs7/**",
"ui/node_modules/**",
"enos/modules/k8s_deploy_vault/raft-config.hcl",
"plugins/database/postgresql/scram/**"
]
}
1 change: 1 addition & 0 deletions .github/scripts/generate-test-package-lists.sh
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ test_packages[13]+=" $base/command/server"
test_packages[13]+=" $base/physical/aerospike"
test_packages[13]+=" $base/physical/cockroachdb"
test_packages[13]+=" $base/plugins/database/postgresql"
test_packages[13]+=" $base/plugins/database/postgresql/scram"
if [ "${ENTERPRISE:+x}" == "x" ] ; then
test_packages[13]+=" $base/vault/external_tests/filteredpathsext"
fi
Expand Down
3 changes: 3 additions & 0 deletions changelog/19616.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
secrets/postgresql: Add configuration to scram-sha-256 encrypt passwords on Vault before sending them to PostgreSQL
```
25 changes: 25 additions & 0 deletions plugins/database/postgresql/passwordauthentication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package postgresql

import "fmt"

// passwordAuthentication determines whether to send passwords in plaintext (password) or hashed (scram-sha-256).
type passwordAuthentication string

var (
// passwordAuthenticationPassword is the default. If set, passwords will be sent to PostgreSQL in plain text.
passwordAuthenticationPassword passwordAuthentication = "password"
passwordAuthenticationSCRAMSHA256 passwordAuthentication = "scram-sha-256"
)

var passwordAuthentications = map[passwordAuthentication]struct{}{
passwordAuthenticationSCRAMSHA256: {},
passwordAuthenticationPassword: {},
}

func parsePasswordAuthentication(s string) (passwordAuthentication, error) {
if _, ok := passwordAuthentications[passwordAuthentication(s)]; !ok {
return "", fmt.Errorf("'%s' is not a valid password authentication type", s)
}

return passwordAuthentication(s), nil
}
57 changes: 43 additions & 14 deletions plugins/database/postgresql/postgresql.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/vault/plugins/database/postgresql/scram"
"github.com/hashicorp/vault/sdk/database/dbplugin/v5"
"github.com/hashicorp/vault/sdk/database/helper/connutil"
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
Expand Down Expand Up @@ -68,7 +69,8 @@ func new() *PostgreSQL {
connProducer.Type = postgreSQLTypeName

db := &PostgreSQL{
SQLConnectionProducer: connProducer,
SQLConnectionProducer: connProducer,
passwordAuthentication: passwordAuthenticationPassword,
}

return db
Expand All @@ -77,7 +79,8 @@ func new() *PostgreSQL {
type PostgreSQL struct {
*connutil.SQLConnectionProducer

usernameProducer template.StringTemplate
usernameProducer template.StringTemplate
passwordAuthentication passwordAuthentication
}

func (p *PostgreSQL) Initialize(ctx context.Context, req dbplugin.InitializeRequest) (dbplugin.InitializeResponse, error) {
Expand Down Expand Up @@ -105,6 +108,20 @@ func (p *PostgreSQL) Initialize(ctx context.Context, req dbplugin.InitializeRequ
return dbplugin.InitializeResponse{}, fmt.Errorf("invalid username template: %w", err)
}

passwordAuthenticationRaw, err := strutil.GetString(req.Config, "password_authentication")
if err != nil {
return dbplugin.InitializeResponse{}, fmt.Errorf("failed to retrieve password_authentication: %w", err)
}

if passwordAuthenticationRaw != "" {
pwAuthentication, err := parsePasswordAuthentication(passwordAuthenticationRaw)
if err != nil {
return dbplugin.InitializeResponse{}, err
}

p.passwordAuthentication = pwAuthentication
}

resp := dbplugin.InitializeResponse{
Config: newConf,
}
Expand Down Expand Up @@ -188,6 +205,15 @@ func (p *PostgreSQL) changeUserPassword(ctx context.Context, username string, ch
"username": username,
"password": password,
}

if p.passwordAuthentication == passwordAuthenticationSCRAMSHA256 {
hashedPassword, err := scram.Hash(password)
if err != nil {
return fmt.Errorf("unable to scram-sha256 password: %w", err)
}
m["password"] = hashedPassword
}
maxcoulombe marked this conversation as resolved.
Show resolved Hide resolved

if err := dbtxn.ExecuteTxQueryDirect(ctx, tx, m, query); err != nil {
return fmt.Errorf("failed to execute query: %w", err)
}
Expand Down Expand Up @@ -272,15 +298,24 @@ func (p *PostgreSQL) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (
}
defer tx.Rollback()

m := map[string]string{
"name": username,
"username": username,
"password": req.Password,
"expiration": expirationStr,
}

if p.passwordAuthentication == passwordAuthenticationSCRAMSHA256 {
hashedPassword, err := scram.Hash(req.Password)
if err != nil {
return dbplugin.NewUserResponse{}, fmt.Errorf("unable to scram-sha256 password: %w", err)
}
m["password"] = hashedPassword
}

for _, stmt := range req.Statements.Commands {
if containsMultilineStatement(stmt) {
// Execute it as-is.
m := map[string]string{
"name": username,
"username": username,
"password": req.Password,
"expiration": expirationStr,
}
if err := dbtxn.ExecuteTxQueryDirect(ctx, tx, m, stmt); err != nil {
return dbplugin.NewUserResponse{}, fmt.Errorf("failed to execute query: %w", err)
}
Expand All @@ -293,12 +328,6 @@ func (p *PostgreSQL) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (
continue
}

m := map[string]string{
"name": username,
"username": username,
"password": req.Password,
"expiration": expirationStr,
}
if err := dbtxn.ExecuteTxQueryDirect(ctx, tx, m, query); err != nil {
return dbplugin.NewUserResponse{}, fmt.Errorf("failed to execute query: %w", err)
}
Expand Down
92 changes: 92 additions & 0 deletions plugins/database/postgresql/postgresql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing"
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
"github.com/hashicorp/vault/sdk/helper/template"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -93,6 +94,97 @@ func TestPostgreSQL_Initialize_ConnURLWithDSNFormat(t *testing.T) {
}
}

// TestPostgreSQL_PasswordAuthentication tests that the default "password_authentication" is "none", and that
// an error is returned if an invalid "password_authentication" is provided.
func TestPostgreSQL_PasswordAuthentication(t *testing.T) {
cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster")
defer cleanup()

dsnConnURL, err := dbutil.ParseURL(connURL)
assert.NoError(t, err)
db := new()

ctx := context.Background()

t.Run("invalid-password-authentication", func(t *testing.T) {
connectionDetails := map[string]interface{}{
"connection_url": dsnConnURL,
"password_authentication": "invalid-password-authentication",
}

req := dbplugin.InitializeRequest{
Config: connectionDetails,
VerifyConnection: true,
}

_, err := db.Initialize(ctx, req)
assert.EqualError(t, err, "'invalid-password-authentication' is not a valid password authentication type")
})

t.Run("default-is-none", func(t *testing.T) {
connectionDetails := map[string]interface{}{
"connection_url": dsnConnURL,
}

req := dbplugin.InitializeRequest{
Config: connectionDetails,
VerifyConnection: true,
}

_ = dbtesting.AssertInitialize(t, db, req)
assert.Equal(t, passwordAuthenticationPassword, db.passwordAuthentication)
})
}

// TestPostgreSQL_PasswordAuthentication_SCRAMSHA256 tests that password_authentication works when set to scram-sha-256.
// When sending an encrypted password, the raw password should still successfully authenticate the user.
func TestPostgreSQL_PasswordAuthentication_SCRAMSHA256(t *testing.T) {
cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster")
defer cleanup()

dsnConnURL, err := dbutil.ParseURL(connURL)
if err != nil {
t.Fatal(err)
}

connectionDetails := map[string]interface{}{
"connection_url": dsnConnURL,
"password_authentication": string(passwordAuthenticationSCRAMSHA256),
}

req := dbplugin.InitializeRequest{
Config: connectionDetails,
VerifyConnection: true,
}

db := new()
resp := dbtesting.AssertInitialize(t, db, req)
assert.Equal(t, string(passwordAuthenticationSCRAMSHA256), resp.Config["password_authentication"])

if !db.Initialized {
t.Fatal("Database should be initialized")
}

ctx := context.Background()
newUserRequest := dbplugin.NewUserRequest{
Statements: dbplugin.Statements{
Commands: []string{
`
CREATE ROLE "{{name}}" WITH
LOGIN
PASSWORD '{{password}}'
VALID UNTIL '{{expiration}}';
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";`,
},
},
Password: "somesecurepassword",
Expiration: time.Now().Add(1 * time.Minute),
}
newUserResponse, err := db.NewUser(ctx, newUserRequest)

assertCredsExist(t, db.ConnectionURL, newUserResponse.Username, newUserRequest.Password)
}
raymonstah marked this conversation as resolved.
Show resolved Hide resolved

func TestPostgreSQL_NewUser(t *testing.T) {
type testCase struct {
req dbplugin.NewUserRequest
Expand Down
21 changes: 21 additions & 0 deletions plugins/database/postgresql/scram/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Taishi Kasuga

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
86 changes: 86 additions & 0 deletions plugins/database/postgresql/scram/scram.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package scram
raymonstah marked this conversation as resolved.
Show resolved Hide resolved

//
// @see https://github.com/postgres/postgres/blob/c30f54ad732ca5c8762bb68bbe0f51de9137dd72/src/interfaces/libpq/fe-auth.c#L1167-L1285
// @see https://github.com/postgres/postgres/blob/e6bdfd9700ebfc7df811c97c2fc46d7e94e329a2/src/interfaces/libpq/fe-auth-scram.c#L868-L905
// @see https://github.com/postgres/postgres/blob/c30f54ad732ca5c8762bb68bbe0f51de9137dd72/src/port/pg_strong_random.c#L66-L96
// @see https://github.com/postgres/postgres/blob/e6bdfd9700ebfc7df811c97c2fc46d7e94e329a2/src/common/scram-common.c#L160-L274
// @see https://github.com/postgres/postgres/blob/e6bdfd9700ebfc7df811c97c2fc46d7e94e329a2/src/common/scram-common.c#L27-L85

// Implementation from https://github.com/supercaracal/scram-sha-256/blob/d3c05cd927770a11c6e12de3e3a99c3446a1f78d/main.go
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"

"golang.org/x/crypto/pbkdf2"
)

const (
// @see https://github.com/postgres/postgres/blob/e6bdfd9700ebfc7df811c97c2fc46d7e94e329a2/src/include/common/scram-common.h#L36-L41
saltSize = 16

// @see https://github.com/postgres/postgres/blob/c30f54ad732ca5c8762bb68bbe0f51de9137dd72/src/include/common/sha2.h#L22
digestLen = 32

// @see https://github.com/postgres/postgres/blob/e6bdfd9700ebfc7df811c97c2fc46d7e94e329a2/src/include/common/scram-common.h#L43-L47
iterationCnt = 4096
)

var (
clientRawKey = []byte("Client Key")
serverRawKey = []byte("Server Key")
)

func genSalt(size int) ([]byte, error) {
salt := make([]byte, size)
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
return nil, err
}
return salt, nil
}

func encodeB64(src []byte) (dst []byte) {
dst = make([]byte, base64.StdEncoding.EncodedLen(len(src)))
base64.StdEncoding.Encode(dst, src)
Copy link
Contributor

Choose a reason for hiding this comment

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

Out of curiosity, why not just return []byte(base64.StdEncoding.EncodeToString(dst, src))?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh, most of the source code in this file is copied over from here.

Not opposed to changing it, including handling the errors you mentioned below.

return
}

func getHMACSum(key, msg []byte) []byte {
h := hmac.New(sha256.New, key)
_, _ = h.Write(msg)
Copy link
Contributor

@cipherboy cipherboy Mar 20, 2023

Choose a reason for hiding this comment

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

HMAC and SHA-256 may error with BoringCrypto fwiw.

I believe this mostly occurs with large inputs, which this shouldn't really have in general, but still worth noting.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good to know. The docs for the functions in the Go standard library say they will "never" return an error.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, the Go standard library doesn't really document the BoringCrypto behavior from what I've seen, especially since its not officially supported.

return h.Sum(nil)
}

func getSHA256Sum(key []byte) []byte {
h := sha256.New()
_, _ = h.Write(key)
return h.Sum(nil)
}

func hashPassword(rawPassword, salt []byte, iter, keyLen int) string {
digestKey := pbkdf2.Key(rawPassword, salt, iter, keyLen, sha256.New)
clientKey := getHMACSum(digestKey, clientRawKey)
storedKey := getSHA256Sum(clientKey)
serverKey := getHMACSum(digestKey, serverRawKey)

return fmt.Sprintf("SCRAM-SHA-256$%d:%s$%s:%s",
iter,
string(encodeB64(salt)),
string(encodeB64(storedKey)),
string(encodeB64(serverKey)),
)
}

func Hash(password string) (string, error) {
salt, err := genSalt(saltSize)
if err != nil {
return "", err
}

hashedPassword := hashPassword([]byte(password), salt, iterationCnt, digestLen)
return hashedPassword, nil
}
27 changes: 27 additions & 0 deletions plugins/database/postgresql/scram/scram_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package scram

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

// TestScram tests the Hash method. The hashed password string should have a SCRAM-SHA-256 prefix.
func TestScram(t *testing.T) {
tcs := map[string]struct {
Password string
}{
"empty-password": {Password: ""},
"simple-password": {Password: "password"},
}

for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
got, err := Hash(tc.Password)
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(got, "SCRAM-SHA-256$4096:"))
assert.Len(t, got, 133)
})
}
}
Loading