-
Notifications
You must be signed in to change notification settings - Fork 1.1k
SCRAM verifier & md5 credential storage with SCRAM/md5 backend pass-through - PostgreSQL #5865
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
Open
rahim-kanji
wants to merge
16
commits into
v3.0
Choose a base branch
from
v3.0_pgsql-auth-5863
base: v3.0
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,199
−37
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
c20df55
feat(pgsql): Phase 1 frontend — verifier/md5 credential storage, B-fl…
rahim-kanji ee2e311
test(pgsql): unit + integration tests for Phase 1 frontend verifier/m…
rahim-kanji 6b88ac2
feat(pgsql): Phase 2 backend — SCRAM/md5 verifier pass-through authen…
rahim-kanji 776f9a1
test(pgsql): Phase 2 backend SCRAM pass-through integration test
rahim-kanji 6a24127
Clear the harvested SCRAM key
rahim-kanji 02249f8
Addressed AI review comments
rahim-kanji 6f05b94
Addressed AI review comments
rahim-kanji 5793528
Fixed test sorting
rahim-kanji 3e54c6c
test(pgsql): backend-kill regression for verifier/md5 users (#5865 re…
renecannao 6c2cd07
test(pgsql): mid-SCRAM-handshake credential reload behavior (#5865 re…
renecannao e03e897
test(pgsql): pool isolation after SCRAM password rotation (#5865 revi…
renecannao 26ef5b1
test(pgsql): md5 backend pass-through + md5 infra user (#5865 review …
renecannao 80f0d5a
test(pgsql): patched-libpq SCRAM/md5 conninfo param validation (#5865…
renecannao 03f78dd
test(pgsql): restore original pgsql-authentication_method in verifier…
renecannao 13903af
fix(pgsql): pass verifier/md5 credentials on the backend kill connection
rahim-kanji f4c9b10
Merge remote-tracking branch 'test/pgsql-5865-tests-wt' into v3.0_pgs…
rahim-kanji File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| --- ../tmp/src/interfaces/libpq/libpq-int.h 2026-06-24 03:51:37.037786331 +0500 | ||
| +++ ./src/interfaces/libpq/libpq-int.h 2026-06-24 03:03:14.441153729 +0500 | ||
| @@ -384,6 +384,9 @@ | ||
| char *pgpassfile; /* path to a file containing password(s) */ | ||
| char *channel_binding; /* channel binding mode | ||
| * (require,prefer,disable) */ | ||
| + char *scram_client_key; /* base64 32-byte ClientKey (ProxySQL SCRAM pass-through) */ | ||
| + char *scram_server_key; /* base64 32-byte ServerKey (ProxySQL SCRAM pass-through) */ | ||
| + char *md5_secret; /* stored "md5"+32hex to reuse for backend md5 auth */ | ||
| char *keepalives; /* use TCP keepalives? */ | ||
| char *keepalives_idle; /* time between TCP keepalives */ | ||
| char *keepalives_interval; /* time between TCP keepalive | ||
| --- ../tmp/src/interfaces/libpq/fe-connect.c 2026-06-24 03:51:37.040811504 +0500 | ||
| +++ ./src/interfaces/libpq/fe-connect.c 2026-06-24 03:04:47.178883449 +0500 | ||
| @@ -361,6 +361,18 @@ | ||
| "Load-Balance-Hosts", "", 8, /* sizeof("disable") = 8 */ | ||
| offsetof(struct pg_conn, load_balance_hosts)}, | ||
|
|
||
| + {"scram_client_key", NULL, NULL, NULL, | ||
| + "SCRAM-Client-Key", "*", 64, | ||
| + offsetof(struct pg_conn, scram_client_key)}, | ||
| + | ||
| + {"scram_server_key", NULL, NULL, NULL, | ||
| + "SCRAM-Server-Key", "*", 64, | ||
| + offsetof(struct pg_conn, scram_server_key)}, | ||
| + | ||
| + {"md5_secret", NULL, NULL, NULL, | ||
| + "MD5-Secret", "*", 64, | ||
| + offsetof(struct pg_conn, md5_secret)}, | ||
| + | ||
| /* Terminating entry --- MUST BE LAST */ | ||
| {NULL, NULL, NULL, NULL, | ||
| NULL, NULL, 0} | ||
| @@ -4436,6 +4436,22 @@ | ||
| } | ||
| free(conn->pgpassfile); | ||
| free(conn->channel_binding); | ||
| + /* ProxySQL SCRAM/md5 verifier pass-through: free + scrub the injected key material. */ | ||
| + if (conn->scram_client_key) | ||
| + { | ||
| + explicit_bzero(conn->scram_client_key, strlen(conn->scram_client_key)); | ||
| + free(conn->scram_client_key); | ||
| + } | ||
| + if (conn->scram_server_key) | ||
| + { | ||
| + explicit_bzero(conn->scram_server_key, strlen(conn->scram_server_key)); | ||
| + free(conn->scram_server_key); | ||
| + } | ||
| + if (conn->md5_secret) | ||
| + { | ||
| + explicit_bzero(conn->md5_secret, strlen(conn->md5_secret)); | ||
| + free(conn->md5_secret); | ||
| + } | ||
| free(conn->keepalives); | ||
| free(conn->keepalives_idle); | ||
| free(conn->keepalives_interval); | ||
| --- ../tmp/src/interfaces/libpq/fe-auth-scram.c 2026-06-24 03:51:37.041568997 +0500 | ||
| +++ ./src/interfaces/libpq/fe-auth-scram.c 2026-06-24 03:08:23.927939654 +0500 | ||
| @@ -120,6 +120,29 @@ | ||
| return NULL; | ||
| } | ||
|
|
||
| + /* | ||
| + * ProxySQL SCRAM pass-through: when a ClientKey is injected the exchange | ||
| + * uses it instead of a password. Require BOTH ClientKey and ServerKey (or | ||
| + * neither), so mutual authentication can never be silently skipped. | ||
| + */ | ||
| + { | ||
| + bool has_ck = (conn->scram_client_key && conn->scram_client_key[0]); | ||
| + bool has_sk = (conn->scram_server_key && conn->scram_server_key[0]); | ||
| + | ||
| + if (has_ck != has_sk) | ||
| + { | ||
| + free(state->sasl_mechanism); | ||
| + free(state); | ||
| + return NULL; | ||
| + } | ||
| + if (has_ck) | ||
| + { | ||
| + /* No password to normalize; keys are injected. */ | ||
| + state->password = NULL; | ||
| + return state; | ||
| + } | ||
| + } | ||
| + | ||
| /* Normalize the password with SASLprep, if possible */ | ||
| rc = pg_saslprep(password, &prep_password); | ||
| if (rc == SASLPREP_OOM) | ||
| @@ -785,14 +808,37 @@ | ||
| * Calculate SaltedPassword, and store it in 'state' so that we can reuse | ||
| * it later in verify_server_signature. | ||
| */ | ||
| - if (scram_SaltedPassword(state->password, state->hash_type, | ||
| - state->key_length, state->salt, state->saltlen, | ||
| - state->iterations, state->SaltedPassword, | ||
| - errstr) < 0 || | ||
| - scram_ClientKey(state->SaltedPassword, state->hash_type, | ||
| - state->key_length, ClientKey, errstr) < 0 || | ||
| - scram_H(ClientKey, state->hash_type, state->key_length, | ||
| - StoredKey, errstr) < 0) | ||
| + if (state->conn->scram_client_key && state->conn->scram_client_key[0]) | ||
| + { | ||
| + /* | ||
| + * ProxySQL SCRAM pass-through: use the injected ClientKey directly and | ||
| + * derive StoredKey = SHA256(ClientKey). Skips SASLprep + PBKDF2. | ||
| + */ | ||
| + int dec = pg_b64_decode(state->conn->scram_client_key, | ||
| + strlen(state->conn->scram_client_key), | ||
| + (char *) ClientKey, state->key_length); | ||
| + | ||
| + if (dec != state->key_length) | ||
| + { | ||
| + *errstr = "invalid scram_client_key"; | ||
| + pg_hmac_free(ctx); | ||
| + return false; | ||
| + } | ||
| + if (scram_H(ClientKey, state->hash_type, state->key_length, | ||
| + StoredKey, errstr) < 0) | ||
| + { | ||
| + pg_hmac_free(ctx); | ||
| + return false; | ||
| + } | ||
| + } | ||
| + else if (scram_SaltedPassword(state->password, state->hash_type, | ||
| + state->key_length, state->salt, state->saltlen, | ||
| + state->iterations, state->SaltedPassword, | ||
| + errstr) < 0 || | ||
| + scram_ClientKey(state->SaltedPassword, state->hash_type, | ||
| + state->key_length, ClientKey, errstr) < 0 || | ||
| + scram_H(ClientKey, state->hash_type, state->key_length, | ||
| + StoredKey, errstr) < 0) | ||
| { | ||
| /* errstr is already filled here */ | ||
| pg_hmac_free(ctx); | ||
| @@ -847,8 +893,22 @@ | ||
| return false; | ||
| } | ||
|
|
||
| - if (scram_ServerKey(state->SaltedPassword, state->hash_type, | ||
| - state->key_length, ServerKey, errstr) < 0) | ||
| + if (state->conn->scram_server_key && state->conn->scram_server_key[0]) | ||
| + { | ||
| + /* ProxySQL SCRAM pass-through: verify with the injected ServerKey. */ | ||
| + int dec = pg_b64_decode(state->conn->scram_server_key, | ||
| + strlen(state->conn->scram_server_key), | ||
| + (char *) ServerKey, state->key_length); | ||
| + | ||
| + if (dec != state->key_length) | ||
| + { | ||
| + *errstr = "invalid scram_server_key"; | ||
| + pg_hmac_free(ctx); | ||
| + return false; | ||
| + } | ||
| + } | ||
| + else if (scram_ServerKey(state->SaltedPassword, state->hash_type, | ||
| + state->key_length, ServerKey, errstr) < 0) | ||
| { | ||
| /* errstr is filled already */ | ||
| pg_hmac_free(ctx); | ||
| --- ../tmp/src/interfaces/libpq/fe-auth.c 2026-06-24 03:51:37.042103163 +0500 | ||
| +++ ./src/interfaces/libpq/fe-auth.c 2026-06-24 03:08:34.430125390 +0500 | ||
| @@ -553,7 +553,8 @@ | ||
| password = conn->connhost[conn->whichhost].password; | ||
| if (password == NULL) | ||
| password = conn->pgpass; | ||
| - if (password == NULL || password[0] == '\0') | ||
| + if ((password == NULL || password[0] == '\0') && | ||
| + !(conn->scram_client_key && conn->scram_client_key[0])) | ||
| { | ||
| appendPQExpBufferStr(&conn->errorMessage, | ||
| PQnoPasswordSupplied); | ||
| @@ -731,9 +732,20 @@ | ||
| } | ||
|
|
||
| crypt_pwd2 = crypt_pwd + MD5_PASSWD_LEN + 1; | ||
| - if (!pg_md5_encrypt(password, conn->pguser, | ||
| - strlen(conn->pguser), crypt_pwd2, | ||
| - &errstr)) | ||
| + if (conn->md5_secret && conn->md5_secret[0]) | ||
| + { | ||
| + /* ProxySQL: reuse the stored md5 secret as the inner hash. */ | ||
| + if (strlen(conn->md5_secret) != MD5_PASSWD_LEN) | ||
| + { | ||
| + libpq_append_conn_error(conn, "invalid md5_secret"); | ||
| + free(crypt_pwd); | ||
| + return STATUS_ERROR; | ||
| + } | ||
| + strcpy(crypt_pwd2, conn->md5_secret); | ||
| + } | ||
| + else if (!pg_md5_encrypt(password, conn->pguser, | ||
| + strlen(conn->pguser), crypt_pwd2, | ||
| + &errstr)) | ||
| { | ||
| libpq_append_conn_error(conn, "could not encrypt password: %s", errstr); | ||
| free(crypt_pwd); | ||
| @@ -1096,7 +1108,8 @@ | ||
| password = conn->connhost[conn->whichhost].password; | ||
| if (password == NULL) | ||
| password = conn->pgpass; | ||
| - if (password == NULL || password[0] == '\0') | ||
| + if ((password == NULL || password[0] == '\0') && | ||
| + !(conn->md5_secret && conn->md5_secret[0])) | ||
| { | ||
| appendPQExpBufferStr(&conn->errorMessage, | ||
| PQnoPasswordSupplied); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Invalidate pooled backends when SCRAM credentials rotate.
Preserving the existing reuse semantics leaves the confirmed case where an A-era backend connection can serve after verifier B is loaded. Include a credential generation/fingerprint in pool matching and evict mismatched idle connections during credential reload; otherwise rotation or revocation does not take effect.
🤖 Prompt for AI Agents