scram

package
v0.0.0-...-b0a47e1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 27, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package scram implements SCRAM-SHA-256 authentication for PostgreSQL protocol connections.

Overview

This package provides SCRAM-SHA-256 authentication for both server and client roles, enabling Multigres to verify client credentials and extract SCRAM keys for passthrough authentication to backend PostgreSQL servers. This eliminates the need to store plaintext passwords while maintaining compatibility with PostgreSQL's native authentication.

SCRAM-SHA-256 Protocol

SCRAM (Salted Challenge Response Authentication Mechanism) is defined in RFC 5802: https://datatracker.ietf.org/doc/html/rfc5802

PostgreSQL's SCRAM-SHA-256 implementation is documented at: https://www.postgresql.org/docs/current/sasl-authentication.html

The protocol involves a three-message exchange:

  1. Client → Server: client-first-message (username, nonce)
  2. Server → Client: server-first-message (combined nonce, salt, iterations)
  3. Client → Server: client-final-message (proof)
  4. Server → Client: server-final-message (server signature for mutual auth)

Why Not Use an Existing Library?

Several Go SCRAM libraries exist (xdg-go/scram, lib/pq, jackc/pgx), but none support our critical requirement: ClientKey extraction for passthrough authentication.

Existing libraries:

  • xdg-go/scram: Most comprehensive, but lacks ClientKey extraction and context.Context support
  • lib/pq: Maintenance mode, client-side only
  • jackc/pgx: Client library, no server-side SCRAM

Our implementation adds:

  • ExtractAndVerifyClientProof: Recovers ClientKey from client's proof for passthrough auth
  • context.Context support: Allows timeout/cancellation during credential lookup
  • Minimum security thresholds: Enforces 4096+ iterations and 8+ byte salts

These features enable Multigres to verify clients and reuse extracted keys to authenticate to backend PostgreSQL servers without storing plaintext passwords.

Architecture

The package is organized into several components:

  • ScramAuthenticator: Stateful server-side authenticator handling the protocol exchange
  • SCRAMClient: Client-side authenticator supporting password and passthrough modes
  • PasswordHashProvider: Interface for retrieving SCRAM password hashes from storage
  • Cryptographic functions: RFC 5802 compliant key derivation and verification
  • Protocol parsers/generators: Message construction and parsing (unexported)

Usage Example

// Server-side authentication
provider := NewMyPasswordProvider()
auth := scram.NewScramAuthenticator(provider, "mydb")

// Start SASL negotiation
mechanisms := auth.StartAuthentication()
// Send AuthenticationSASL with mechanisms...

// Handle client-first-message
serverFirst, err := auth.HandleClientFirst(ctx, clientFirstMsg)
// Send AuthenticationSASLContinue with serverFirst...

// Handle client-final-message
serverFinal, err := auth.HandleClientFinal(clientFinalMsg)
if auth.IsAuthenticated() {
    // Authentication successful
    clientKey, serverKey := auth.ExtractedKeys()
    // Use keys for passthrough authentication...
}

Key Passthrough Authentication

A critical feature of this implementation is extracting the ClientKey from the client's proof during authentication. This enables SCRAM passthrough:

  1. Client authenticates to Multigres multigateway
  2. Multigateway extracts ClientKey from the authentication proof
  3. Multigateway uses ClientKey to authenticate to PostgreSQL as that user
  4. No plaintext password needed at any stage

This is possible because SCRAM proofs reveal the ClientKey through XOR:

ClientKey = ClientProof XOR ClientSignature

The extracted ClientKey can then be used with the stored ServerKey to perform subsequent SCRAM authentications to backend PostgreSQL servers without knowing the original password.

Password Hash Storage

This package expects password hashes in PostgreSQL's SCRAM-SHA-256 format:

SCRAM-SHA-256$<iterations>:<salt>$<StoredKey>:<ServerKey>

The PasswordHashProvider interface abstracts the storage mechanism:

type PasswordHashProvider interface {
    GetPasswordHash(ctx context.Context, username, database string) (*ScramHash, error)
}

Implementations can:

  • Query PostgreSQL's pg_authid directly
  • Use a credential cache with TTL and invalidation
  • Fetch from a centralized credential service
  • Combine multiple sources with fallback logic

Future Directions

Current implementation:

  • Core SCRAM protocol and cryptography
  • Server-side authentication (ScramAuthenticator)
  • Client-side authentication with passthrough support (SCRAMClient)
  • No credential caching
  • No integration with multigateway/multipooler

Planned enhancements:

  • Caching password hashes in multigateway to save a round-trip to multipooler on connect
  • Integration with multigateway/multipooler for end-to-end passthrough

Credential Cache Design Considerations

When implementing credential caching:

  • TTL: Balance security (shorter) vs performance (longer)

    PgBouncer: No cache, runs auth_query on every connection See src/client.c start_auth_query() https://github.com/pgbouncer/pgbouncer/tree/master/src

    Supavisor: 24-hour cache with 15-second background refresh + refresh on auth failure See lib/supavisor/secret_cache.ex @default_secrets_ttl, fetch_validation_secrets/3 See lib/supavisor/secret_checker.ex @interval, check_secrets/2 (background polling) See lib/supavisor/client_handler/auth.ex check_and_update_secrets/7 (refresh on auth failure) https://github.com/supabase/supavisor/tree/main/lib/supavisor

  • Invalidation: Consider cache busting on password changes Option 1: PostgreSQL triggers + notification channel Option 2: Periodic refresh on access Option 3: External invalidation API

Security Considerations

  • All password hash comparisons use constant-time algorithms (crypto/subtle)
  • Nonce validation prevents replay attacks
  • State machine prevents protocol violations
  • No plaintext passwords stored or logged
  • ClientKey extraction requires successful authentication

Password Normalization (SASLprep)

This implementation includes SASLprep password normalization (RFC 4013) for full PostgreSQL compatibility. SASLprep applies NFKC Unicode normalization and character mapping to passwords before hashing.

Key behaviors:

  • Non-ASCII spaces normalized to ASCII space (U+0020)
  • Soft hyphens and zero-width characters removed
  • Unicode combining characters normalized
  • Fallback to raw password on normalization failure (prohibited chars, bidi violations)

This matches PostgreSQL's lenient approach: passwords that fail SASLprep validation (invalid UTF-8, prohibited characters, bidirectional check failures) are accepted using their raw byte representation.

Compatibility

This implementation is compatible with:

  • PostgreSQL 10+ SCRAM-SHA-256 authentication
  • Standard PostgreSQL client libraries (psql, libpq, pgx, etc.)
  • PostgreSQL's pg_authid password hash format
  • PostgreSQL's SASLprep implementation (RFC 4013)

Not currently supported:

  • SCRAM-SHA-1 (deprecated, not used by PostgreSQL)
  • Channel binding (SCRAM-SHA-256-PLUS)
  • Custom iteration counts (uses hash's iteration count)

References

Index

Constants

View Source
const (
	// ScramSHA256Prefix is the prefix for SCRAM-SHA-256 password hashes in PostgreSQL.
	ScramSHA256Prefix = "SCRAM-SHA-256"

	// MinIterationCount is the minimum PBKDF2 iteration count accepted for security.
	// RFC 5802 recommends a minimum of 4096 iterations to make brute-force attacks harder.
	MinIterationCount = 4096

	// MinSaltLength is the minimum salt length in bytes accepted for security.
	MinSaltLength = 8
)
View Source
const (
	// ScramSHA256Mechanism is the SASL mechanism name for SCRAM-SHA-256.
	ScramSHA256Mechanism = "SCRAM-SHA-256"
)

Variables

View Source
var (
	// ErrUserNotFound indicates the user does not exist.
	ErrUserNotFound = errors.New("user not found")

	// ErrAuthenticationFailed indicates the password proof was invalid.
	ErrAuthenticationFailed = errors.New("authentication failed")
)

Sentinel errors for SCRAM authentication.

Functions

func ComputeClientKey

func ComputeClientKey(saltedPassword []byte) []byte

ComputeClientKey computes ClientKey = HMAC(SaltedPassword, "Client Key").

func ComputeClientSignature

func ComputeClientSignature(storedKey []byte, authMessage string) []byte

ComputeClientSignature computes ClientSignature = HMAC(StoredKey, AuthMessage).

func ComputeSaltedPassword

func ComputeSaltedPassword(password string, salt []byte, iterations int) []byte

ComputeSaltedPassword computes the SCRAM SaltedPassword using PBKDF2. SaltedPassword = Hi(Normalize(password), salt, iterations) Where Hi is PBKDF2 with HMAC-SHA-256.

Passwords are normalized using SASLprep (RFC 4013), which applies stringprep with NFKC normalization and character mapping. If normalization fails (invalid UTF-8, prohibited characters, bidirectional check failures), the raw password is used unchanged, matching PostgreSQL's lenient behavior.

func ComputeServerKey

func ComputeServerKey(saltedPassword []byte) []byte

ComputeServerKey computes ServerKey = HMAC(SaltedPassword, "Server Key").

func ComputeServerSignature

func ComputeServerSignature(serverKey []byte, authMessage string) []byte

ComputeServerSignature computes ServerSignature = HMAC(ServerKey, AuthMessage).

func ComputeStoredKey

func ComputeStoredKey(clientKey []byte) []byte

ComputeStoredKey computes StoredKey = H(ClientKey) where H is SHA-256.

func ExtractAndVerifyClientProof

func ExtractAndVerifyClientProof(storedKey []byte, authMessage string, clientProof []byte) ([]byte, error)

ExtractAndVerifyClientProof verifies the client's proof and extracts the ClientKey. This is used for SCRAM key passthrough: after verifying a client, we can use the extracted ClientKey to authenticate as that user to PostgreSQL.

The verification process: 1. Compute ClientSignature = HMAC(StoredKey, AuthMessage) 2. Recover ClientKey = ClientProof XOR ClientSignature 3. Compute RecoveredStoredKey = H(ClientKey) 4. Verify RecoveredStoredKey == StoredKey

Returns the extracted ClientKey on successful verification (error == nil). Returns ErrAuthenticationFailed if the proof is invalid (wrong password). Returns other errors for unexpected conditions (malformed proof, length mismatches). Uses constant-time comparison to prevent timing attacks.

func IsScramSHA256Hash

func IsScramSHA256Hash(hash string) bool

IsScramSHA256Hash returns true if the hash string appears to be a SCRAM-SHA-256 hash. This is a quick check based on the prefix; it does not validate the entire format.

Types

type PasswordHashProvider

type PasswordHashProvider interface {
	// GetPasswordHash retrieves the SCRAM-SHA-256 hash for a user in a database.
	// Returns ErrUserNotFound if the user does not exist.
	GetPasswordHash(ctx context.Context, username, database string) (*ScramHash, error)
}

PasswordHashProvider is an interface for retrieving password hashes. This abstraction allows the authenticator to be used with different storage backends (PostgreSQL, cache, etc.).

type SCRAMClient

type SCRAMClient struct {
	// contains filtered or unexported fields
}

SCRAMClient implements client-side SCRAM-SHA-256 authentication. It supports two modes: 1. Password mode: authenticate using a plaintext password 2. Passthrough mode: authenticate using pre-extracted ClientKey/ServerKey

Passthrough mode enables SCRAM key passthrough: after a proxy verifies a client, it can extract the ClientKey and use it to authenticate to the backend database without knowing the plaintext password.

func NewSCRAMClientWithKeys

func NewSCRAMClientWithKeys(username string, clientKey, serverKey []byte) *SCRAMClient

NewSCRAMClientWithKeys creates a SCRAM client that authenticates with extracted SCRAM keys. This enables SCRAM passthrough: a proxy can verify a client, extract the ClientKey, and use it to authenticate to PostgreSQL without the plaintext password.

The clientKey and serverKey should be extracted from a previous SCRAM authentication using ExtractAndVerifyClientProof and the hash's ServerKey.

func NewSCRAMClientWithPassword

func NewSCRAMClientWithPassword(username, password string) *SCRAMClient

NewSCRAMClientWithPassword creates a SCRAM client that authenticates with a password. This is the standard mode where the password is used to derive SCRAM keys.

func (*SCRAMClient) ClientFirstMessage

func (c *SCRAMClient) ClientFirstMessage() (string, error)

ClientFirstMessage generates the client-first-message to send to the server. This starts the SCRAM authentication handshake. Returns the full message including the GS2 header.

func (*SCRAMClient) ProcessServerFirst

func (c *SCRAMClient) ProcessServerFirst(serverFirst string) (string, error)

ProcessServerFirst processes the server-first-message and generates the client-final-message. The serverFirst parameter is the server's response to the client-first-message. Returns the client-final-message to send to the server.

func (*SCRAMClient) VerifyServerFinal

func (c *SCRAMClient) VerifyServerFinal(serverFinal string) error

VerifyServerFinal verifies the server-final-message for mutual authentication. The serverFinal parameter is the server's response to the client-final-message. Returns nil if the server signature is valid, or an error if verification fails.

type ScramAuthenticator

type ScramAuthenticator struct {
	// contains filtered or unexported fields
}

ScramAuthenticator handles SCRAM-SHA-256 authentication. It implements the server side of the SCRAM protocol as defined in RFC 5802.

Usage:

  1. Call StartAuthentication() to get the list of supported mechanisms.
  2. After receiving client-first-message, call HandleClientFirst().
  3. After receiving client-final-message, call HandleClientFinal().
  4. Check IsAuthenticated() to see if auth succeeded.
  5. Call ExtractedKeys() to get SCRAM keys for passthrough authentication.

The authenticator maintains state between calls and enforces valid state transitions to prevent protocol errors.

Thread Safety: ScramAuthenticator is NOT thread-safe. Each connection must use its own authenticator instance. Do not share authenticators across goroutines or reuse them for multiple concurrent authentication attempts. The Reset() method allows reusing an authenticator sequentially, but only after the previous authentication has completed.

func NewScramAuthenticator

func NewScramAuthenticator(provider PasswordHashProvider, database string) *ScramAuthenticator

NewScramAuthenticator creates a new SCRAM authenticator with the given password hash provider and database name for credential lookup.

Panics if provider is nil.

func (*ScramAuthenticator) AuthenticatedUser

func (a *ScramAuthenticator) AuthenticatedUser() string

AuthenticatedUser returns the username that was successfully authenticated. Returns an empty string if authentication has not completed successfully.

func (*ScramAuthenticator) ExtractedKeys

func (a *ScramAuthenticator) ExtractedKeys() (clientKey, serverKey []byte)

ExtractedKeys returns the SCRAM keys extracted during authentication. These can be used for passthrough authentication to backend PostgreSQL servers.

ClientKey is recovered from the client's proof (ClientKey = ClientProof XOR ClientSignature). ServerKey comes from the stored password hash.

Returns nil, nil if authentication has not completed successfully.

func (*ScramAuthenticator) HandleClientFinal

func (a *ScramAuthenticator) HandleClientFinal(clientFinalMessage string) (string, error)

HandleClientFinal processes the client-final-message from the client. Returns the server-final-message to send back on success.

The client-final-message contains the channel binding, combined nonce, and client proof. This method verifies the proof and, if valid, returns the server signature for mutual authentication.

Returns ErrAuthenticationFailed if the proof is invalid.

func (*ScramAuthenticator) HandleClientFirst

func (a *ScramAuthenticator) HandleClientFirst(ctx context.Context, clientFirstMessage, startupMessageUsername string) (string, error)

HandleClientFirst processes the client-first-message from the client. Returns the server-first-message to send back.

The client-first-message comes from SASLInitialResponse and contains the GS2 header and client-first-message-bare (username, client nonce).

The startupMessageUsername is used when the client sends an empty username in the client-first-message. PostgreSQL allows this and uses the username from the startup message. This parameter should be the username from the startup message.

This method looks up the user's password hash and generates the server-first-message containing the combined nonce, salt, and iteration count.

func (*ScramAuthenticator) IsAuthenticated

func (a *ScramAuthenticator) IsAuthenticated() bool

IsAuthenticated returns true if the authentication completed successfully.

func (*ScramAuthenticator) Reset

func (a *ScramAuthenticator) Reset()

Reset clears the authenticator state, allowing it to be reused for another authentication attempt.

func (*ScramAuthenticator) StartAuthentication

func (a *ScramAuthenticator) StartAuthentication() []string

StartAuthentication begins the SCRAM authentication process. Returns the list of supported SASL mechanisms.

This corresponds to sending AuthenticationSASL (auth type 10) to the client.

type ScramHash

type ScramHash struct {
	// Iterations is the PBKDF2 iteration count used to derive the salted password.
	Iterations int

	// Salt is the random salt used in PBKDF2 key derivation.
	Salt []byte

	// StoredKey is H(ClientKey) where H is SHA-256 and ClientKey = HMAC(SaltedPassword, "Client Key").
	// Used to verify the client's proof.
	StoredKey []byte

	// ServerKey is HMAC(SaltedPassword, "Server Key").
	// Used to generate the server's signature for mutual authentication.
	ServerKey []byte
}

ScramHash contains the parsed components of a PostgreSQL SCRAM-SHA-256 password hash. The hash format is: SCRAM-SHA-256$<iterations>:<salt>$<StoredKey>:<ServerKey> where salt, StoredKey, and ServerKey are base64-encoded.

func ParseScramSHA256Hash

func ParseScramSHA256Hash(hash string) (*ScramHash, error)

ParseScramSHA256Hash parses a PostgreSQL SCRAM-SHA-256 password hash string. The expected format is: SCRAM-SHA-256$<iterations>:<salt>$<StoredKey>:<ServerKey>

Example: SCRAM-SHA-256$4096:W22ZaJ0SNY7soEsUEjb6gQ==$WG5d8oPm3OtcPnkdi4Oln6rNiYzlYY42lUpMtdJ7U90=:HKZfkuYXDxJboM9DFNR0yFNHpRx/rbdVdNOTk/V0v0Q=

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL