feat(pgserver): Phase 5 — password + MD5 authentication

Trust mode (v1.0 default) accepts anyone; that's fine for embedded
demo but unshipping a multi-client database without credentials
would be irresponsible. This commit adds two of libpq's three
standard auth flows. SCRAM-SHA-256 is Phase 5.1 — pgx/psql both
fall back to MD5 cleanly when the server advertises only md5, so
v1.0's functional coverage is complete with the pair landed here.

Auth subsystem
--------------

`hbrtl/pgserver/auth.go` adds:

* An in-memory role registry: `roleMap map[string]*role` guarded by
  sync.RWMutex. Reads (lookupRole) are hot-path during connection
  startup so the RWMutex lets multiple sessions auth in parallel
  without serialising through a plain Mutex.

* `AddRole(name, password)` / `RemoveRole(name)` Go API consumed
  by the new HB_FUNCs `PG_ADD_ROLE` / `PG_REMOVE_ROLE` (see
  register.go). Bootstrap PRG idiom:

      PG_ADD_ROLE("alice", "swordfish")
      PG_ADD_ROLE("bob",   "hunter2")
      PG_SERVER_START(":5432", "md5")

* `authPassword()` — cleartext PasswordMessage exchange. The wire
  payload is plain so intended for TLS-protected links only;
  Phase 6 ties the warning to actual TLS detection on the session.

* `authMD5()` — libpq's md5 challenge:

      server → AuthenticationMD5Password{salt: 4 random bytes}
      client → "md5" || md5_hex( md5_hex(password || user) || salt )

  We recompute the canonical hash from the stored plaintext and
  compare. md5Challenge() is exported for pinning by a Go unit
  test (vector cross-checked against libpq's fe-auth-md5.c).

Salt is sourced from crypto/rand on every challenge so replay
attacks against a captured wire trace can't reuse a prior hash.

Dispatch matrix (Config.AuthMode → flow):
  "" / "trust" → AuthenticationOk immediately, no lookup
  "password"   → authPassword()
  "md5"        → authMD5()
  anything else→ 28000 + connection close

Tests
-----

Unit (hbrtl/pgserver/pgserver_test.go):
  PASS  TestMD5Challenge           (vector + determinism + diff)
  PASS  TestRoleRegistry           (add/replace/remove/lookup)

Integration (tests/pgserver/run.sh):
  PASS  Simple Query: SELECT 1, 'hello'
  PASS  Multi-statement Simple Query
  PASS  Transaction control: BEGIN/COMMIT round-trip
  PASS  MD5 auth: wrong password rejected
  PASS  MD5 auth: correct password accepted

End-to-end matrix with real psql:
  wrong password   → "ERROR: md5 authentication failed for user 'alice'"
  correct password → SELECT returns row
  unknown user     → "ERROR: md5 authentication failed for user 'eve'"
  password mode    → cleartext exchange works equivalently

All six release gates green:
  go test ./...               ✓
  FiveSql2 SQL:1999 43/43     ✓
  Harbour compat 56/56        ✓
  std.ch 17/17                ✓
  FRB 7/7                     ✓
  pgserver integration 5/5    ✓ (up from 3/3 in Phase 4)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 14:01:30 +09:00
parent 8472928102
commit 90eafcfc06
4 changed files with 270 additions and 12 deletions

View File

@@ -110,6 +110,40 @@ else
fail "Transaction control: BEGIN/COMMIT round-trip" "$out"
fi
# 4) MD5 authentication — kill the trust-mode server, restart with
# md5 + a known role, then verify both the rejection and success
# paths.
kill $SERVER_PID 2>/dev/null
wait 2>/dev/null
cat > "$work/auth.prg" <<EOF
PROCEDURE Main()
PG_ADD_ROLE( "alice", "swordfish" )
PG_SERVER_START( ":$PORT", "md5" )
RETURN
EOF
"$FIVE" build "$work/auth.prg" "$ROOT/_FiveSql2/src/"*.prg -o "$work/auth" >/dev/null 2>&1
"$work/auth" &
SERVER_PID=$!
sleep 1
trap "kill $SERVER_PID 2>/dev/null; rm -rf '$work'" EXIT
bad="$(PGPASSWORD=wrong psql "postgres://alice@127.0.0.1:$PORT/alice?sslmode=disable" \
-c "SELECT 1" 2>&1 | head -1 || true)"
if echo "$bad" | grep -qi "md5 authentication failed"; then
ok "MD5 auth: wrong password rejected"
else
fail "MD5 auth: wrong password rejected" "$bad"
fi
good="$(PGPASSWORD=swordfish psql "postgres://alice@127.0.0.1:$PORT/alice?sslmode=disable" \
-c "SELECT 'ok' AS x" -At 2>&1 || true)"
if echo "$good" | grep -q "^ok$"; then
ok "MD5 auth: correct password accepted"
else
fail "MD5 auth: correct password accepted" "$good"
fi
echo "================================================================"
echo " pgserver integration: $pass / $total passed"
echo "================================================================"