Skip to content

sql,security: create a password hash abstraction (scram 4/10)#74844

Merged
craig[bot] merged 1 commit intomasterfrom
20211228-scram4
Jan 21, 2022
Merged

sql,security: create a password hash abstraction (scram 4/10)#74844
craig[bot] merged 1 commit intomasterfrom
20211228-scram4

Conversation

@knz
Copy link
Copy Markdown
Contributor

@knz knz commented Jan 14, 2022

(PR peeled away from #74301; previous PR in series #74843; next PR in series: #74845)
Epic CRDB-5349

Prior to this commit, password hashes loaded from storage
were simply represented as byte arrays.

The in-RAM credential cache was then simply storing these byte arrays.

Keeping this simple encoding is unfortunate for the SCRAM
authentication format, because it forces the password check code to
re-parse the 4 parameters of the SCRAM credentials upon every
authentication event.

Instead, we want to pre-parse the parameters once, and store them
pre-parsed in the credential cache.

To achieve this, this commit introduces a new security.PasswordHash
to replace []byte to represent password hashes in memory.
A stored byte array can be converted to a security.PasswordHash
using the new security.LoadPasswordHash() function.

Additionally, this commit performs the following cleanups:

  • it renames security.CompareHashAndPassword to
    CompareHashAndCleartextPassword to clarify the format
    the password is expected in.
  • it makes CompareHashAndCleartextPassword() return
    a boolean alongside the error. The function does not
    return an error any more if the hash is valid but the
    credentials don't match. Callers are now expected to
    propagate the returned error, if any.
  • it un-exports security.ErrPasswordUserAuthFailed, which
    was a formatting string, and replaces it by a new
    error constructor NewErrPasswordUserAuthFailed (this
    was meant to be used to construct errors originally).

NB: this commit merely adds a new abstraction in the APIs.
SCRAM hashes are still not used for authentication.

Release note: None

@cockroach-teamcity
Copy link
Copy Markdown
Member

This change is Reviewable

@knz knz force-pushed the 20211228-scram3 branch from eee689b to 796f659 Compare January 14, 2022 10:40
@knz knz force-pushed the 20211228-scram4 branch from daf3bf7 to 03128b5 Compare January 14, 2022 10:42
@knz knz changed the title wip4 sql,security: create a password hash abstraction Jan 14, 2022
@knz knz requested a review from rafiss January 14, 2022 10:43
@knz
Copy link
Copy Markdown
Contributor Author

knz commented Jan 14, 2022

@rafiss this is the part you were already reviewing in #74301, extracted here as a single commit for your reviewing convenience.

cc @catj-cockroach , this is what I told you about.

@knz knz marked this pull request as ready for review January 14, 2022 10:44
@knz knz requested a review from a team January 14, 2022 10:44
@knz knz changed the title sql,security: create a password hash abstraction sql,security: create a password hash abstraction (scram 4/10) Jan 14, 2022
@knz knz force-pushed the 20211228-scram4 branch from 03128b5 to 56202e9 Compare January 14, 2022 15:08
@knz knz force-pushed the 20211228-scram3 branch from 796f659 to fd18de4 Compare January 14, 2022 19:17
@knz knz force-pushed the 20211228-scram4 branch 2 times, most recently from 6243645 to db869c0 Compare January 14, 2022 19:46
Copy link
Copy Markdown
Collaborator

@rafiss rafiss left a comment

Choose a reason for hiding this comment

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

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @knz and @rafiss)


pkg/security/password.go, line 349 at r1 (raw file):

}

func makeSCRAMHash(storedHash []byte, parts [][]byte, invalidHash PasswordHash) PasswordHash {

can this function document the order of the parts slice? would it be too cumbersome to make it accept 4 different []byte params or a struct, so that it's self-documenting instead?

@knz knz force-pushed the 20211228-scram4 branch from db869c0 to a965cf0 Compare January 15, 2022 12:11
Copy link
Copy Markdown
Contributor Author

@knz knz left a comment

Choose a reason for hiding this comment

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

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @jeffswenson and @rafiss)


pkg/security/password.go, line 349 at r1 (raw file):

Previously, rafiss (Rafi Shamim) wrote…

can this function document the order of the parts slice? would it be too cumbersome to make it accept 4 different []byte params or a struct, so that it's self-documenting instead?

Changed the data type to hide the details. This is not meant to be used without isSCRAMHash().
(I split the two upon a suggestion by @jeffswenson in this comment but the latter is not meant to be used standalone)

@knz knz force-pushed the 20211228-scram3 branch from fd18de4 to 35f27d3 Compare January 15, 2022 12:38
@knz knz requested a review from a team January 15, 2022 12:38
@knz knz requested a review from a team as a code owner January 15, 2022 12:38
@knz knz force-pushed the 20211228-scram4 branch from a965cf0 to 2a97dd4 Compare January 15, 2022 12:39
@knz
Copy link
Copy Markdown
Contributor Author

knz commented Jan 19, 2022

@rafiss anything else you'd like to happen here?

Copy link
Copy Markdown
Collaborator

@rafiss rafiss left a comment

Choose a reason for hiding this comment

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

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @jeffswenson, @knz, and @rafiss)


pkg/security/password.go, line 331 at r2 (raw file):

// isSCRAMHash() to makeSCRAMHash(), so that the latter cannot be
// legitimately used without the former.
type scramParts [][]byte

what do you think about making this

type scramParts [5][]byte

pkg/security/password.go, line 335 at r2 (raw file):

func isSCRAMHash(inputPassword []byte) (bool, scramParts) {
	parts := scramHashRe.FindSubmatch(inputPassword)
	return parts != nil, scramParts(parts)

and then here we can convert the parts with

if len(parts) != 5 {
  return false, scramParts{}
}
return true, *(*scramParts)(parts)

(new in go 1.17 https://tip.golang.org/ref/spec#Conversions_from_slice_to_array_pointer)


pkg/security/password.go, line 357 at r2 (raw file):

// previous call to isSCRAMHash().
func makeSCRAMHash(storedHash []byte, parts scramParts, invalidHash PasswordHash) PasswordHash {
	iters, err := strconv.Atoi(string(parts[1]))

could we add a few functions on the scramParts type?

func (sp scramParts) getIters (int, error) {
  return strconv.Atoi(string(sp[1]))
}

func (sp scramParts) getSalt (string, error) {
  return base64.StdEncoding.DecodeString(string(sp[2]))
}

func (sp scramParts) getStoredKey (string, error) {
  return base64.StdEncoding.DecodeString(string(sp[3]))
}

func (sp scramParts) getServerKey (string, error) {
  return base64.StdEncoding.DecodeString(string(sp[4]))
}

Copy link
Copy Markdown
Contributor Author

@knz knz left a comment

Choose a reason for hiding this comment

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

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @jeffswenson and @rafiss)


pkg/security/password.go, line 331 at r2 (raw file):

Previously, rafiss (Rafi Shamim) wrote…

what do you think about making this

type scramParts [5][]byte

Done.


pkg/security/password.go, line 335 at r2 (raw file):

Previously, rafiss (Rafi Shamim) wrote…

and then here we can convert the parts with

if len(parts) != 5 {
  return false, scramParts{}
}
return true, *(*scramParts)(parts)

(new in go 1.17 https://tip.golang.org/ref/spec#Conversions_from_slice_to_array_pointer)

Done.


pkg/security/password.go, line 357 at r2 (raw file):

Previously, rafiss (Rafi Shamim) wrote…

could we add a few functions on the scramParts type?

func (sp scramParts) getIters (int, error) {
  return strconv.Atoi(string(sp[1]))
}

func (sp scramParts) getSalt (string, error) {
  return base64.StdEncoding.DecodeString(string(sp[2]))
}

func (sp scramParts) getStoredKey (string, error) {
  return base64.StdEncoding.DecodeString(string(sp[3]))
}

func (sp scramParts) getServerKey (string, error) {
  return base64.StdEncoding.DecodeString(string(sp[4]))
}

Done.

Copy link
Copy Markdown
Collaborator

@rafiss rafiss left a comment

Choose a reason for hiding this comment

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

looks great!

Reviewed 5 of 11 files at r1, 6 of 6 files at r3, all commit messages.
Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @jeffswenson and @rafiss)

Copy link
Copy Markdown
Collaborator

@jeffswenson jeffswenson left a comment

Choose a reason for hiding this comment

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

LGTM

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @jeffswenson and @rafiss)

@knz
Copy link
Copy Markdown
Contributor Author

knz commented Jan 20, 2022

NB: I'm holding off merging this PR until all the PRs in the sequence are ready (otherwise, the entire chain will be auto-closed by github).

Copy link
Copy Markdown
Contributor Author

@knz knz left a comment

Choose a reason for hiding this comment

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

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @jeffswenson and @rafiss)


pkg/security/password.go, line 335 at r2 (raw file):

Previously, knz (kena) wrote…

Done.

FYI: it works in the code, but the linter hates it

    lint_test.go:1740: err=exit status 2, stderr=panic: in github.com/cockroachdb/cockroach/pkg/security.isSCRAMHash: cannot convert Load <[][]byte> t0 ([][]byte) to *github.com/cockroachdb/cockroach/pkg/security.scramParts
        goroutine 5661 [running]:
        honnef.co/go/tools/go/ir.emitConv(0xc0a64a30e0, {0x974b68, 0xc0ab54e300}, {0x9656e0, 0xc03bb74c70}, {0x964e20, 0xc009336000})
          /go/src/github.com/cockroachdb/cockroach/vendor/honnef.co/go/tools/go/ir/emit.go:261 +0xb25
        honnef.co/go/tools/go/ir.(*builder).expr0(0xc0aa34db00, 0xc0a64a30e0, {0x966a00, 0xc009336000}, {0x7, {0x9656e0, 0xc03bb74c70}, {0x0, 0x0}})
          /go/src/github.com/cockroachdb/cockroach/vendor/honnef.co/go/tools/go/ir/builder.go:544 +0xa77

@knz knz force-pushed the 20211228-scram4 branch from 4281bba to e616c13 Compare January 20, 2022 16:15
Prior to this commit, password hashes loaded from storage
were simply represented as byte arrays.

The in-RAM credential cache was then simply storing these byte arrays.

Keeping this simple encoding is unfortunate for the SCRAM
authentication format, because it forces the password check code to
re-parse the 4 parameters of the SCRAM credentials upon every
authentication event.

Instead, we want to pre-parse the parameters once, and store them
pre-parsed in the credential cache.

To achieve this, this commit introduces a new `security.PasswordHash`
to replace `[]byte` to represent password hashes in memory.
A stored byte array can be converted to a `security.PasswordHash`
using the new `security.LoadPasswordHash()` function.

Additionally, this commit performs the following cleanups:

- it renames `security.CompareHashAndPassword` to
  `CompareHashAndCleartextPassword` to clarify the format
  the password is expected in.
- it makes `CompareHashAndCleartextPassword()` return
  a boolean alongside the error. The function does not
  return an error any more if the hash is valid but the
  credentials don't match. Callers are now expected to
  propagate the returned error, if any.
- it un-exports `security.ErrPasswordUserAuthFailed`, which
  was a formatting string, and replaces it by a new
  error constructor `NewErrPasswordUserAuthFailed` (this
  was meant to be used to construct errors originally).

NB: this commit merely adds a new abstraction in the APIs.
SCRAM hashes are still not used for authentication.

Release note: None
@knz knz force-pushed the 20211228-scram3 branch from f3ee5e9 to b822b1b Compare January 21, 2022 00:00
@knz knz force-pushed the 20211228-scram4 branch from e616c13 to a28002d Compare January 21, 2022 00:00
craig bot pushed a commit that referenced this pull request Jan 21, 2022
74301: sql,server: support SCRAM authentication for SQL sessions r=rafiss,JeffSwenson,bdarnell,aaron-crl,kpatron-cockroachlabs a=knz

Fixes #42519.
Fixes #74511.
Informs #65117.
Epic CRDB-5349

**tldr:** this PR adds support for the SCRAM-SHA-256 authentication method, as defined by IETF RFCs [5802](https://datatracker.ietf.org/doc/html/rfc5802) and [7677](https://datatracker.ietf.org/doc/html/rfc7677) and as supported by PostgreSQL. This offers [multiple security benefits](#scram-benefits) over CockroachDB's current use of cleartext password authentication. To use this feature,
1) the stored password hashes must use the SCRAM encoding (this requires a migration away from crdb's bcrypt-based hashes); and
2) one of the SCRAM authentication methods must be enabled explictly via the HBA configuration (cluster setting `server.host_based_authentication.configuration`).

### How to review this work

The addition of the feature goes as follows:

1. adding HBA syntax and authentication method hooks for `scram-sha-256` and `cert-scram-sha-256`. These are gated behind a new cluster version, so that previous-version nodes do not get confused by the new syntax.

   Split into separate PR: #74842

2. extending ALTER/CREATE USER/ROLE WITH PASSWORD to recognize SCRAM hashes. This allows operators to use these SQL statements to populate SCRAM hashes.
    (This should also be seen as the more desirable way to configure SQL user accounts: it ensures that the CockroachDB node never ever sees the cleartext password of an account. Even if we later support computing the SCRAM hash server-side, pre-computing the hash client-side should be seen and documented as the superior approach.)

   Split into separate PR: #74843

3. extending the existing cleartext methods inside CockroachDB (used for the HTTP interface and the `password`-based methods in SQL) to compare a cleartext password to a pre-computed SCRAM hash. This ensures that the existing password mechanisms that are cleartext-based continue to work even after the stored credentials start using the SCRAM encoding.

   Split into separate PRs: #74844 and #74845

4. extending the code created at step 1 to actually use the stored SCRAM credentials for client authentication. This achieves the main goal.

   Split into separate PRs: #74846 and #74847

5. making the hash method used to encode cleartext passwords passed to ALTER/CREATE USER/ROLE WITH PASSWORD configurable, using a new cluster setting `server.user_login.password_encryption`, and exposing its value via the pg-compatible session variable `password_encryption`.
   This is a convenience feature - properly secured deployments should not need this, as they should use pre-hashing client-side instead (see note at point 2 above).

   Split into separate PR: #74848

6. making crdb auto-select SCRAM authn if the stored password uses SCRAM. This is one step to ensuring that previous-version clusters are automatically opted into SCRAM.

   Split into separate PR: #74849

7. Auto-upgrade the stored credentials from bcrypt to SCRAM, when enabled and possible.

   Split into separate PR: #74850

The reviewer is invited to consider each PR in turn, read its commit message(s) to understand the goal of that PR, then review the PR to ascertain that the implementation (and relevant tests) match the goals announced in the commit message.


Co-authored-by: Raphael 'kena' Poss <knz@thaumogen.net>
Base automatically changed from 20211228-scram3 to master January 21, 2022 09:12
@craig craig bot merged commit a28002d into master Jan 21, 2022
@craig craig bot deleted the 20211228-scram4 branch January 21, 2022 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants