Skip to content

fix: enforce RFC5802 channel binding negotiation and proof length in SCRAM - #476

Merged
sunng87 merged 4 commits into
masterfrom
fix/scram-channel-binding-and-proof-length
Oct 21, 2026
Merged

sunng87 merged 4 commits into
masterfrom
fix/scram-channel-binding-and-proof-length

Conversation

@sunng87

@sunng87 sunng87 commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes two SCRAM strictness gaps surfaced by greptimedb's review of the SCRAM implementation. Both were confirmed with proof-of-concept reproductions before fixing.

1. Unsupported channel binding no longer accepted (RFC 5802 §6)

The client-first parser accepts any p=<type> gs2-cbind-flag, and compute_channel_binding falls back to base64-of-header for unknown binding types (or when no server certificate is configured). As a result, a client could request an unsupported channel binding on the plain SCRAM-SHA-256 mechanism and still authenticate, with c= carrying no binding data — PoC authenticated with p=tls-unique,,n=user,... against a server with no certificate configured.

Upon receipt of the client-first message, the server now rejects:

  • any p flag when no server certificate is configured (no binding supported at all)
  • p flags naming a binding type other than tls-server-end-point (the only supported type)
  • the y flag while the server supports channel binding — per RFC 5802 §6 this indicates a downgrade attack (the -PLUS mechanism was advertised but stripped from the negotiation). Without this rule, advertising SCRAM-SHA-256-PLUS provides no downgrade protection.

The check lives in on_client_first_message_with_secret, so it covers both on_client_first_message and the message-level process_scram_message path, as well as direct low-level users of ScramServerAuth.

2. Overlong SCRAM proof rejected on the verifier path

The Verifier path decoded p= proof with no length check, and xor zips its operands — silently truncating to 32 bytes. A valid 32-byte proof with appended junk still verified (PoC: 35-byte proof = 32 valid + 3 junk bytes authenticated). The SaltedPassword path was already safe (exact base64 comparison).

The proof must now be exactly one hash output long (stored_key.len()).

Tests

  • channel binding rejected when not configured (p=tls-server-end-point, p=tls-unique, p=tls-exporter)
  • unknown binding type rejected when configured; y rejected when configured but still accepted without a certificate; n still accepted when configured
  • verifier-path roundtrip (new), overlong-proof rejected, short-proof rejected

Notes

  • ⚠️ Behavior change: y-flag clients are now rejected when a certificate is configured. pgwire currently advertises SCRAM-SHA-256-PLUS whenever a certificate is configured, even on non-TLS connections — rejecting y there is RFC-correct (a compliant client must use p= when -PLUS is offered), but a possible follow-up is to stop advertising -PLUS on non-TLS connections like postgres does (it only advertises it over SSL).
  • Downstream adopters (greptimedb) should pick this up via a rev bump.

…SCRAM

Two strictness gaps reported by greptimedb's review of the SCRAM
implementation, both confirmed with proof-of-concept reproductions:

Channel binding negotiation was not enforced (RFC 5802 section 6).
The client-first parser accepts any "p=<type>" gs2-cbind-flag and
compute_channel_binding falls back to base64-of-header for unknown
types or when no server certificate is configured, so a client could
request an unsupported binding on the plain SCRAM-SHA-256 mechanism
and still authenticate with c= carrying no binding data. Upon receipt
of the client-first message the server now rejects:

- any "p" flag when no server certificate is configured
- "p" flags naming a binding type other than tls-server-end-point
- the "y" flag while the server supports channel binding, which
  indicates a downgrade attack (the -PLUS mechanism was advertised
  but stripped from the negotiation)

Overlong SCRAM proofs were accepted on the verifier path. The proof
was decoded without a length check and xor zips its operands, so a
valid 32-byte proof with appended junk still verified. The proof must
now be exactly one hash output long (stored_key.len()).

Adds regression tests for both cases, plus verifier-path roundtrip
coverage.
The new channel-binding and proof-length tests use the bare/without_proof
helpers, which were gated behind the client-api feature; ungate them so
the default and minimal feature sets compile.

Also apply stable rustfmt formatting to the new test code.
@sunng87
sunng87 merged commit 6740d6e into master Oct 21, 2026
11 checks passed
@sunng87
sunng87 deleted the fix/scram-channel-binding-and-proof-length branch October 21, 2026 08:29
sunng87 added a commit to sunng87/greptimedb that referenced this pull request Oct 21, 2026
…binding clients

- Fix MixedAuthProvider to return a SCRAM verifier for the denied user,
  so the test exercises rejection by authorization after successful
  SCRAM verification; record the selected mechanism from the returned
  variant and assert authorize ran exactly once per verified user.
- Assert the denied connection fails with 28P01 before any terminal
  message, protecting the completion-ordering introduced by pgwire's
  authentication gate.
- Add a raw-protocol test proving a client-first with a p= channel
  binding flag is rejected (RFC 5802 section 6), while n-flag clients
  (tokio_postgres) continue normally.

Bump pgwire to 0.42.1, which enforces RFC 5802 channel binding
negotiation and rejects short/overlong SCRAM proofs on the verifier
path (sunng87/pgwire#476).

Signed-off-by: Ning Sun <sunning@greptime.com>
MarkkuPekkarinen pushed a commit to MarkkuPekkarinen/greptimedb that referenced this pull request Oct 21, 2026
…timeTeam#9634)

* feat: use pgwire auth mechanisms API to simplify startuphandler

* chore: update pgwire

* chore: update pgwire revision to merged one

* refactor: model based on upstream api

* refactor: simplify implementation by introducing authsource

* refactor: update to latest pgwire apis

* refactor: adopt pgwire api changes

* chore: switch to released pgwire

* test: harden pg scram mixed-auth integration test and reject channel-binding clients

- Fix MixedAuthProvider to return a SCRAM verifier for the denied user,
  so the test exercises rejection by authorization after successful
  SCRAM verification; record the selected mechanism from the returned
  variant and assert authorize ran exactly once per verified user.
- Assert the denied connection fails with 28P01 before any terminal
  message, protecting the completion-ordering introduced by pgwire's
  authentication gate.
- Add a raw-protocol test proving a client-first with a p= channel
  binding flag is rejected (RFC 5802 section 6), while n-flag clients
  (tokio_postgres) continue normally.

Bump pgwire to 0.42.1, which enforces RFC 5802 channel binding
negotiation and rejects short/overlong SCRAM proofs on the verifier
path (sunng87/pgwire#476).

Signed-off-by: Ning Sun <sunning@greptime.com>

---------

Signed-off-by: Ning Sun <sunning@greptime.com>
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.

1 participant