Skip to content

feat(postgres): add SCRAM hashing and password type detection - #260

Merged
gbartolini merged 7 commits into
cloudnative-pg:mainfrom
leonardoce:scram
May 22, 2026
Merged

gbartolini merged 7 commits into
cloudnative-pg:mainfrom
leonardoce:scram

Conversation

@leonardoce

Copy link
Copy Markdown
Contributor

Introduce two new sibling packages under pkg/postgres:

  • scram implements PostgreSQL's SCRAM-SHA-256 password hash. It can both generate a hash in the canonical "SCRAM-SHA-256$:$:" form and verify a plaintext password against an existing hash. The comparison is constant-time, and credential derivation uses xdg-go/scram's GetStoredCredentialsWithError so that PBKDF2 failures surface as errors instead of panics.

  • password classifies a value stored in pg_authid.rolpassword as plaintext, MD5, or SCRAM-SHA-256, mirroring get_password_type in PostgreSQL's src/backend/libpq/crypt.c and the structural check in parse_scram_secret from src/backend/libpq/auth-scram.c. This lets callers decide whether a proposed password is already encoded or still in plaintext before deciding how to handle it.

leonardoce and others added 4 commits May 22, 2026 10:03
Introduce two new sibling packages under pkg/postgres:

* `scram` implements PostgreSQL's SCRAM-SHA-256 password hash. It can
  both generate a hash in the canonical
  "SCRAM-SHA-256$<iter>:<salt>$<StoredKey>:<ServerKey>" form and verify
  a plaintext password against an existing hash. The comparison is
  constant-time, and credential derivation uses
  xdg-go/scram's GetStoredCredentialsWithError so that PBKDF2 failures
  surface as errors instead of panics.

* `password` classifies a value stored in `pg_authid.rolpassword` as
  plaintext, MD5, or SCRAM-SHA-256, mirroring `get_password_type` in
  PostgreSQL's `src/backend/libpq/crypt.c` and the structural check in
  `parse_scram_secret` from `src/backend/libpq/auth-scram.c`. This lets
  callers decide whether a proposed password is already encoded or
  still in plaintext before deciding how to handle it.

Signed-off-by: Leonardo Cecchi <[email protected]>
…y trust

Tighten the hash parser used by Verify so it agrees with the structural
check performed by the password classifier (package password):

  - reject iteration counts <= 0 via the new ErrInvalidIterations
    sentinel (was: any int accepted, including 0 and negatives)
  - reject StoredKey / ServerKey values that do not decode to exactly
    32 bytes via the new ErrInvalidStoredKey / ErrInvalidServerKey
    sentinels (was: any base64-decodable length accepted, with the
    mismatch caught later via the length-mismatch short-circuit in
    subtle.ConstantTimeCompare)

Also clarify the wording of the two existing structural error
sentinels — "first block" / "second block" required the reader to
remember the canonical SCRAM-SHA-256$<iter>:<salt>$<StoredKey>:<ServerKey>
layout to know which is which.

Document on Verify that the iteration count parsed from the hash drives
the PBKDF2 work. Callers that may receive attacker-controlled hashes
should validate or cap that count before invoking Verify; the library
does not impose a hard maximum.

Test additions cover the four new error paths and tighten the existing
"non-base64 server key" entry so it no longer trips the new
StoredKey-length check before reaching the ServerKey decode.

Signed-off-by: Armando Ruocco <[email protected]>
Documentation-only follow-up: expand the package doc, normalize the
Err* sentinels to "is returned" and have each name the structural
rule it covers, document Verify's return contract, move the canonical
hash format into parsePostgreSQLHash's godoc, cite the upstream
defaults sources, switch GenerateOptions to Go's "// FieldName" form,
and fix the "have not have been" typo on Defaults.

Assisted-by: Claude Opus 4.7

Signed-off-by: Gabriele Bartolini <[email protected]>
Export the SCRAM-SHA-256 key length from pkg/postgres/password and
consume it from pkg/postgres/scram, removing the duplicated unexported
constant.

Assisted-by: Claude Opus 4.7

Signed-off-by: Gabriele Bartolini <[email protected]>
mnencia added 3 commits May 22, 2026 11:27
PostgreSQL's parse_scram_secret reads the iteration count with strtol
into a C int, so any hash a server accepts fits in 32 bits. Switch from
strconv.Atoi (which on amd64 accepts values up to ~9.2e18) to
strconv.ParseInt with bit size 32, so we reject the same overflow cases
libpq does and no others.

Signed-off-by: Marco Nenciarini <[email protected]>
Defaults treated zero as "use DefaultPostgresIterations" but passed any
other value, including negatives, straight through to xdg-go/scram,
where it produced an opaque downstream error. Reject negatives early
with ErrInvalidIterations, matching what parsePostgreSQLHash already
does on the read side.

Signed-off-by: Marco Nenciarini <[email protected]>
Generate previously called the exported Defaults method, which wrote the
random salt and default iteration count back onto the receiver. A
caller that left Salt unset and invoked Generate twice on the same
options got identical hashes, because the first call had silently
filled in the salt.

Unexport Defaults as applyDefaults and have Generate operate on a local
copy of the receiver, so repeated calls draw independent salts and the
caller's struct is never modified. Drop the direct Defaults tests and
fold their assertions into observable Generate behavior, plus a
regression guard that asserts the receiver is unchanged across calls.

Signed-off-by: Marco Nenciarini <[email protected]>
@gbartolini
gbartolini merged commit 24a5474 into cloudnative-pg:main May 22, 2026
4 checks passed
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