Skip to content

fix(scram): replace hand-rolled SCRAM client with postgres-protocol - #8

Merged
birdmanmandbir merged 2 commits into
mainfrom
fix/scram-postgres-protocol
Apr 8, 2026
Merged

fix(scram): replace hand-rolled SCRAM client with postgres-protocol#8
birdmanmandbir merged 2 commits into
mainfrom
fix/scram-postgres-protocol

Conversation

@birdmanmandbir

Copy link
Copy Markdown
Contributor

Summary

  • Root cause: src/scram.rs:67 constructed the SCRAM client-final-message without the p= prefix on the proof attribute. Backend rejected every connection with 08P01 malformed SCRAM message, then the proxy mis-decoded the ErrorResponse and reported "early eof" to the client.
  • Fix: Delete the hand-rolled SCRAM module entirely; depend on postgres-protocol = "0.6.11" and use ScramSha256 (the same impl tokio-postgres uses in production).
  • Cleanup: SASL wire-framing helpers move into src/wire.rs with unit tests. Crypto deps hmac, sha2, base64 are dropped.

Verification plan (post-merge)

CI builds and pushes the image; then:

  1. Roll the deployment: kubectl -n supa-dev rollout restart deploy/pgwire-supabase-proxy
  2. Repro the original failing connection from inside the cluster:
    kubectl -n supa-dev run psql-verify --rm -i --restart=Never \
      --image=postgres:16-alpine --command -- \
      psql "postgres://agent:<JWT>@pgwire-supabase-proxy.supa-dev.svc:5432/supabase" \
      -c "SELECT current_user, current_database();"
    
  3. Expected: row with authenticated | supabase (NOT "server closed the connection unexpectedly").
  4. Check ttal log psp — should show client authenticated, backend SCRAM-SHA-256 auth complete, session ready — entering byte-forward mode, and on disconnect a clean connection closed with non-zero byte counts.

If step 2/3 fails, kubectl rollout undo deploy/pgwire-supabase-proxy and re-open the task.

…ScramSha256

The hand-rolled SCRAM-SHA-256 client in src/scram.rs built a malformed
client-final-message: it omitted the mandatory `p=` prefix on the proof
attribute (RFC 5802 §5.1), so the wire bytes were
`c=biws,r=NONCE,<base64proof>` instead of `c=biws,r=NONCE,p=<base64proof>`.
The CNPG backend rejected every connection with FATAL 08P01
"malformed SCRAM message — Expected character '=' for attribute 'Y'",
and the proxy then mis-decoded the resulting ErrorResponse as a SASL
frame ("expected R, got 45"), closing the client with an opaque
"early eof". Every connection from fn-cli through the proxy failed.

Replaces the entire src/scram.rs (~440 LOC of hand-rolled crypto) with
postgres-protocol::authentication::sasl::ScramSha256 — sfackler's
battle-tested implementation that powers tokio-postgres in production.
The three small SASL wire-framing helpers (write_sasl_initial_response,
write_sasl_response, read_sasl_auth_message) move into src/wire.rs
where the rest of the wire framing already lives, with byte-layout
unit tests.

After successful SASL exchange the new code explicitly reads the
AuthenticationOk (type 0) message before falling through to the
parameter-status drain loop, so the loop does not have to silently
swallow an `R` frame.

Drops now-unused crypto dependencies: hmac, sha2, base64. Keeps rand
(still used for BackendKeyData PIDs in proxy.rs).
@birdmanmandbir

Copy link
Copy Markdown
Contributor Author

coder (round 3):

CI checks passed (e47aa7d) — all green: fmt, clippy, unit tests. Ready for review.

@birdmanmandbir

Copy link
Copy Markdown
Contributor Author

pr-review-lead (round 4):

PR Review: fix/scram-postgres-protocol

5 reviewers ran — code, principles, silent-failure-hunter, test-analyzer, comment-analyzer.


✅ Critical Issues — None


✅ Important Issues — None


💡 Suggestions (3 found)

  1. [test-analyzer] read_sasl_auth_message has no unit tests — The write helpers have byte-layout tests but the read path (tag check, auth_type validation, body extraction) is only exercised implicitly by ignored integration tests. Adding unit tests for the error paths (wrong tag, body too short, type mismatch) would catch regressions in the read side of the SCRAM exchange. src/wire.rs

  2. [silent-failure-hunter] Missing debug log on cleartext password auth success — The SCRAM branch logs "backend SCRAM-SHA-256 auth complete" but AuthMethod::CleartextPassword has no corresponding success log. Minor observability gap. src/proxy.rs

  3. [comment-analyzer] read_sasl_auth_message doc comment could be clearer — The comment lists types 11, 12, 0 but only names two of the three. Suggested wording:

    /// - type=11 (AuthenticationSASLContinue)
    /// - type=12 (AuthenticationSASLFinal)
    /// - type=0  (AuthenticationOk)
    

    src/wire.rs


Strengths

  • Core bug confirmed fixed (code-reviewer): The old format\!(",{}", client_proof) was missing p= prefix per RFC 5802 §5.1 — this caused CNPG to reject every connection. The postgres-protocol library handles formatting correctly.
  • Net −400 LOC: 437 lines of hand-rolled SCRAM crypto replaced with a battle-tested library dependency. Significant reduction in maintenance surface.
  • Wire helpers well-placed: Moved to wire.rs where they belong; properly public; good doc comments on the format strings.
  • Error propagation is clean: let _ = wire::read_sasl_auth_message(&mut backend, 0).await? is not a silent failure — ? propagates errors, only the unused Vec<u8> is discarded.
  • Dependency cleanup: hmac 0.12, sha2 0.10, base64 0.22 all removed. Lock file resolves to unified dep versions.
  • Principles clean: DRY, SOLID, KISS, YAGNI all pass. Linear SCRAM flow, no unnecessary abstractions.
  • CI green: fmt, clippy, unit tests all pass.

VERDICT: LGTM

All suggestions are optional quality improvements — none block merge.

- Add unit tests for read_sasl_auth_message error paths:
  wrong tag byte and auth_type mismatch
- Add missing debug log on CleartextPassword auth success
- Improve read_sasl_auth_message doc comment to list all three
  SASL auth type values explicitly
@birdmanmandbir

Copy link
Copy Markdown
Contributor Author

coder (round 5):

Triage Update

Fixed

  • read_sasl_auth_message has no unit tests — added 2 error-path unit tests in src/wire.rs:571-592 (wrong_tag, type_mismatch)
  • Missing debug log on cleartext password auth success — added tracing::debug! in src/proxy.rs:168
  • read_sasl_auth_message doc comment could be clearer — updated to explicitly list all 3 auth type values

Remaining

  • None

All 3 suggestions addressed. CI green: fmt ✓ clippy ✓ 9 tests ✓. Ready to merge.

@birdmanmandbir
birdmanmandbir merged commit 6162942 into main Apr 8, 2026
1 check passed
@birdmanmandbir
birdmanmandbir deleted the fix/scram-postgres-protocol branch April 8, 2026 02:25
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