scram

package module
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Nov 24, 2025 License: Apache-2.0 Imports: 16 Imported by: 221

README

Go Reference Go Report Card Github Actions

scram – Go implementation of RFC-5802

Description

Package scram provides client and server implementations of the Salted Challenge Response Authentication Mechanism (SCRAM) described in

It includes both client and server side support.

Channel binding is supported for SCRAM-PLUS variants, including:

  • tls-unique (RFC 5929) - insecure, but required
  • tls-server-end-point (RFC 5929) - works with all TLS versions
  • tls-exporter (RFC 9266) - recommended for TLS 1.3+

SCRAM message extensions are not supported.

Examples

Client side
package main

import "github.com/xdg-go/scram"

func main() {
    // Get Client with username, password and (optional) authorization ID.
    clientSHA1, err := scram.SHA1.NewClient("mulder", "trustno1", "")
    if err != nil {
        panic(err)
    }

    // Prepare the authentication conversation. Use the empty string as the
    // initial server message argument to start the conversation.
    conv := clientSHA1.NewConversation()
    var serverMsg string

    // Get the first message, send it and read the response.
    firstMsg, err := conv.Step(serverMsg)
    if err != nil {
        panic(err)
    }
    serverMsg = sendClientMsg(firstMsg)

    // Get the second message, send it, and read the response.
    secondMsg, err := conv.Step(serverMsg)
    if err != nil {
        panic(err)
    }
    serverMsg = sendClientMsg(secondMsg)

    // Validate the server's final message.  We have no further message to
    // send so ignore that return value.
    _, err = conv.Step(serverMsg)
    if err != nil {
        panic(err)
    }

    return
}

func sendClientMsg(s string) string {
    // A real implementation would send this to a server and read a reply.
    return ""
}
Client side with channel binding (SCRAM-PLUS)
package main

import (
    "crypto/tls"
    "github.com/xdg-go/scram"
)

func main() {
    // Establish TLS connection
    tlsConn, err := tls.Dial("tcp", "server:port", &tls.Config{MinVersion: tls.VersionTLS13})
    if err != nil {
        panic(err)
    }
    defer tlsConn.Close()

    // Get Client with username, password
    client, err := scram.SHA256.NewClient("mulder", "trustno1", "")
    if err != nil {
        panic(err)
    }

    // Create channel binding from TLS connection (TLS 1.3 example)
    // Use NewTLSExporterBinding for TLS 1.3+, NewTLSServerEndpointBinding for all TLS versions
    channelBinding, err := scram.NewTLSExporterBinding(&tlsConn.ConnectionState())
    if err != nil {
        panic(err)
    }

    // Create conversation with channel binding for SCRAM-SHA-256-PLUS
    conv := client.NewConversationWithChannelBinding(channelBinding)
    // ... rest of authentication conversation
}

Copyright 2018 by David A. Golden. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License"). You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

Documentation

Overview

Package scram provides client and server implementations of the Salted Challenge Response Authentication Mechanism (SCRAM) described in RFC-5802, RFC-7677, and RFC-9266.

Usage

The scram package provides variables, `SHA1`, `SHA256`, and `SHA512`, that are used to construct Client or Server objects.

clientSHA1,   err := scram.SHA1.NewClient(username, password, authID)
clientSHA256, err := scram.SHA256.NewClient(username, password, authID)
clientSHA512, err := scram.SHA512.NewClient(username, password, authID)

serverSHA1,   err := scram.SHA1.NewServer(credentialLookupFcn)
serverSHA256, err := scram.SHA256.NewServer(credentialLookupFcn)
serverSHA512, err := scram.SHA512.NewServer(credentialLookupFcn)

These objects are used to construct ClientConversation or ServerConversation objects that are used to carry out authentication.

clientConv := client.NewConversation()
serverConv := server.NewConversation()

Channel Binding (SCRAM-PLUS)

The scram package supports channel binding for SCRAM-PLUS authentication variants as described in RFC-5802, RFC-5929, and RFC-9266. Channel binding cryptographically binds the SCRAM authentication to an underlying TLS connection, preventing man-in-the-middle attacks.

To use channel binding, create conversations with channel binding data obtained from the TLS connection:

// Client example with tls-exporter (TLS 1.3+)
client, _ := scram.SHA256.NewClient(username, password, "")
channelBinding, _ := scram.NewTLSExporterBinding(&tlsConn.ConnectionState())
clientConv := client.NewConversationWithChannelBinding(channelBinding)

// Server conversation with the same channel binding
server, _ := scram.SHA256.NewServer(credentialLookupFcn)
serverConv := server.NewConversationWithChannelBinding(channelBinding)

Helper functions are provided to create ChannelBinding values from TLS connections:

  • NewTLSServerEndpointBinding: Uses server certificate hash (RFC 5929, all TLS versions)
  • NewTLSExporterBinding: Uses exported keying material (RFC 9266, recommended for TLS 1.3+)

Channel binding is configured on conversations rather than clients or servers because binding data is connection-specific.

Channel binding type negotiation is not defined by the SCRAM protocol. Applications must ensure both client and server agree on the same channel binding type.

Example
package main

import "github.com/xdg-go/scram"

func main() {
	// Get Client with username, password and (optional) authorization ID.
	clientSHA1, err := scram.SHA1.NewClient("mulder", "trustno1", "")
	if err != nil {
		panic(err)
	}

	// Prepare the authentication conversation. Use the empty string as the
	// initial server message argument to start the conversation.
	conv := clientSHA1.NewConversation()
	var serverMsg string

	// Get the first message, send it and read the response.
	firstMsg, err := conv.Step(serverMsg)
	if err != nil {
		panic(err)
	}
	serverMsg = sendClientMsg(firstMsg)

	// Get the second message, send it, and read the response.
	secondMsg, err := conv.Step(serverMsg)
	if err != nil {
		panic(err)
	}
	serverMsg = sendClientMsg(secondMsg)

	// Validate the server's final message.  We have no further message to
	// send so ignore that return value.
	_, err = conv.Step(serverMsg)
	if err != nil {
		panic(err)
	}
}

func sendClientMsg(s string) string {
	// A real implementation would send this to a server and read a reply.
	return ""
}
Example (ChannelBindingModes)

ExampleClient_channelBindingModes demonstrates the three channel binding modes available when creating authentication conversations.

package main

import (
	"fmt"

	"github.com/xdg-go/scram"
)

func main() {
	client, _ := scram.SHA256.NewClient("user", "password", "")

	// Mode 1: No channel binding support
	// Use when: Application doesn't support channel binding at all
	// GS2 header: "n,,"
	conv1 := client.NewConversation()
	msg1, _ := conv1.Step("")
	fmt.Printf("Mode 1 GS2 header: %s\n", msg1[:3])

	// Mode 2: Advertise channel binding support
	// Use when: Application supports CB but server didn't advertise PLUS variants
	// Example: Server advertised "SCRAM-SHA-256" but not "SCRAM-SHA-256-PLUS"
	// GS2 header: "y,,"
	// Security: Helps detect downgrade attacks (MITM stripping PLUS from server list)
	conv2 := client.NewConversationAdvertisingChannelBinding()
	msg2, _ := conv2.Step("")
	fmt.Printf("Mode 2 GS2 header: %s\n", msg2[:3])

	// Mode 3: Use channel binding
	// Use when: Server advertised PLUS variant AND app has TLS connection state
	// GS2 header: "p=<type>,,"
	// Note: In real code, get connState from actual TLS connection
	// var connState *tls.ConnectionState = tlsConn.ConnectionState()
	// cb, _ := scram.NewTLSExporterBinding(connState)
	//
	// For example purposes, create a dummy channel binding.
	cb := scram.ChannelBinding{
		Type: scram.ChannelBindingTLSExporter,
		Data: []byte("example-cb-data"),
	}
	conv3 := client.NewConversationWithChannelBinding(cb)
	msg3, _ := conv3.Step("")
	fmt.Printf("Mode 3 GS2 header: %s\n", msg3[:16])

}
Output:
Mode 1 GS2 header: n,,
Mode 2 GS2 header: y,,
Mode 3 GS2 header: p=tls-exporter,,

Index

Examples

Constants

View Source
const (
	// ErrInvalidEncoding indicates the client message had invalid encoding
	ErrInvalidEncoding = "e=invalid-encoding"

	// ErrExtensionsNotSupported indicates unrecognized 'm' value
	ErrExtensionsNotSupported = "e=extensions-not-supported"

	// ErrInvalidProof indicates the authentication proof from the client was invalid
	ErrInvalidProof = "e=invalid-proof"

	// ErrChannelBindingsDontMatch indicates channel binding data didn't match expected value
	ErrChannelBindingsDontMatch = "e=channel-bindings-dont-match"

	// ErrServerDoesSupportChannelBinding indicates server does support channel
	// binding. This is returned if a downgrade attack is detected or if the
	// client does not support binding and channel binding is required.
	ErrServerDoesSupportChannelBinding = "e=server-does-support-channel-binding"

	// ErrChannelBindingNotSupported indicates channel binding is not supported
	ErrChannelBindingNotSupported = "e=channel-binding-not-supported"

	// ErrUnsupportedChannelBindingType indicates the requested channel binding type is not supported
	ErrUnsupportedChannelBindingType = "e=unsupported-channel-binding-type"

	// ErrUnknownUser indicates the specified user does not exist
	ErrUnknownUser = "e=unknown-user"

	// ErrInvalidUsernameEncoding indicates invalid username encoding (invalid UTF-8 or SASLprep failed)
	ErrInvalidUsernameEncoding = "e=invalid-username-encoding"

	// ErrNoResources indicates the server is out of resources
	ErrNoResources = "e=no-resources"

	// ErrOtherError is a catch-all for unspecified errors. The server may substitute
	// the real reason with this error to prevent information disclosure.
	ErrOtherError = "e=other-error"
)

Server error values as defined in RFC-5802 and RFC-7677. These are returned by the server in error responses as "e=<value>".

Variables

This section is empty.

Functions

This section is empty.

Types

type ChannelBinding added in v1.2.0

type ChannelBinding struct {
	Type ChannelBindingType
	Data []byte
}

ChannelBinding holds the channel binding type and data for SCRAM-PLUS authentication. Use constructors to create type-specific bindings.

func NewTLSExporterBinding added in v1.2.0

func NewTLSExporterBinding(connState *tls.ConnectionState) (ChannelBinding, error)

NewTLSExporterBinding creates a ChannelBinding for tls-exporter channel binding per RFC 9266. It uses TLS Exported Keying Material with the label "EXPORTER-Channel-Binding" and an empty context.

This is the recommended channel binding type for TLS 1.3+.

func NewTLSServerEndpointBinding added in v1.2.0

func NewTLSServerEndpointBinding(connState *tls.ConnectionState) (ChannelBinding, error)

NewTLSServerEndpointBinding creates a ChannelBinding for tls-server-end-point channel binding per RFC 5929. It extracts the server's certificate from the TLS connection state and hashes it using the appropriate hash function based on the certificate's signature algorithm.

This works with all TLS versions including TLS 1.3.

func NewTLSUniqueBinding added in v1.2.0

func NewTLSUniqueBinding(data []byte) ChannelBinding

NewTLSUniqueBinding creates a ChannelBinding for tls-unique channel binding. Since Go's standard library doesn't expose the TLS Finished message, applications must provide the data directly.

Note: tls-unique is considered insecure and should generally be avoided.

func (ChannelBinding) IsSupported added in v1.2.0

func (cb ChannelBinding) IsSupported() bool

IsSupported returns true if the channel binding is configured with a non-empty type and data.

func (ChannelBinding) Matches added in v1.2.0

func (cb ChannelBinding) Matches(other ChannelBinding) bool

Matches returns true if this channel binding matches another channel binding in both type and data.

type ChannelBindingType added in v1.2.0

type ChannelBindingType string

ChannelBindingType represents the type of channel binding to use with SCRAM-PLUS authentication variants. The type must match one of the channel binding types defined in RFC 5056, RFC 5929, or RFC 9266.

const (
	// ChannelBindingNone indicates no channel binding is used.
	ChannelBindingNone ChannelBindingType = ""

	// ChannelBindingTLSUnique uses the TLS Finished message from the first
	// TLS handshake (RFC 5929).  This is considered insecure, but is included
	// as required by RFC 5802.
	ChannelBindingTLSUnique ChannelBindingType = "tls-unique"

	// ChannelBindingTLSServerEndpoint uses a hash of the server's certificate
	// (RFC 5929).  This works with all TLS versions including TLS 1.3.
	ChannelBindingTLSServerEndpoint ChannelBindingType = "tls-server-end-point"

	// ChannelBindingTLSExporter uses TLS Exported Keying Material with the
	// label "EXPORTER-Channel-Binding" (RFC 9266).  This is the recommended
	// channel binding type for TLS 1.3.
	ChannelBindingTLSExporter ChannelBindingType = "tls-exporter"
)

type Client

type Client struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

Client implements the client side of SCRAM authentication. It holds configuration values needed to initialize new client-side conversations for a specific username, password and authorization ID tuple. Client caches the computationally-expensive parts of a SCRAM conversation as described in RFC-5802. If repeated authentication conversations may be required for a user (e.g. disconnect/reconnect), the user's Client should be preserved.

For security reasons, Clients have a default minimum PBKDF2 iteration count of 4096. If a server requests a smaller iteration count, an authentication conversation will error.

A Client can also be used by a server application to construct the hashed authentication values to be stored for a new user. See StoredCredentials() for more.

func (*Client) GetStoredCredentials deprecated

func (c *Client) GetStoredCredentials(kf KeyFactors) StoredCredentials

GetStoredCredentials takes a salt and iteration count structure and provides the values that must be stored by a server to authenticate a user. These values are what the Server credential lookup function must return for a given username.

Deprecated: Use GetStoredCredentialsWithError for proper error handling. This method panics if PBKDF2 key derivation fails, which should only occur with invalid KeyFactors parameters.

func (*Client) GetStoredCredentialsWithError added in v1.2.0

func (c *Client) GetStoredCredentialsWithError(kf KeyFactors) (StoredCredentials, error)

GetStoredCredentialsWithError takes a salt and iteration count structure and provides the values that must be stored by a server to authenticate a user. These values are what the Server credential lookup function must return for a given username.

Returns an error if PBKDF2 key derivation fails, which can occur with invalid parameters in Go 1.24+ (e.g., invalid iteration counts or key lengths).

func (*Client) NewConversation

func (c *Client) NewConversation() *ClientConversation

NewConversation constructs a client-side authentication conversation. Conversations cannot be reused, so this must be called for each new authentication attempt.

func (*Client) NewConversationAdvertisingChannelBinding added in v1.2.0

func (c *Client) NewConversationAdvertisingChannelBinding() *ClientConversation

NewConversationAdvertisingChannelBinding constructs a client-side authentication conversation that advertises channel binding support without using it. This generates the "y" GS2 flag, indicating the client supports channel binding but the server did not advertise a PLUS variant mechanism.

This helps detect downgrade attacks where a MITM strips PLUS mechanism advertisements from the server's mechanism list. If the server actually advertised PLUS variants, it will reject the "y" flag as a downgrade attack.

Use this when:

  • Your application supports channel binding (has access to TLS connection state)
  • SASL mechanism negotiation showed the server does NOT advertise PLUS variants (e.g., server advertised "SCRAM-SHA-256" but not "SCRAM-SHA-256-PLUS")

Conversations cannot be reused, so this must be called for each new authentication attempt.

func (*Client) NewConversationWithChannelBinding added in v1.2.0

func (c *Client) NewConversationWithChannelBinding(cb ChannelBinding) *ClientConversation

NewConversationWithChannelBinding constructs a client-side authentication conversation with channel binding for SCRAM-PLUS authentication. Channel binding is connection-specific, so a new conversation should be created for each connection being authenticated. Conversations cannot be reused, so this must be called for each new authentication attempt.

func (*Client) WithMinIterations

func (c *Client) WithMinIterations(n int) *Client

WithMinIterations changes minimum required PBKDF2 iteration count.

func (*Client) WithNonceGenerator

func (c *Client) WithNonceGenerator(ng NonceGeneratorFcn) *Client

WithNonceGenerator replaces the default nonce generator (base64 encoding of 24 bytes from crypto/rand) with a custom generator. This is provided for testing or for users with custom nonce requirements.

type ClientConversation

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

ClientConversation implements the client-side of an authentication conversation with a server. A new conversation must be created for each authentication attempt.

func (*ClientConversation) Done

func (cc *ClientConversation) Done() bool

Done returns true if the conversation is completed or has errored.

func (*ClientConversation) Step

func (cc *ClientConversation) Step(challenge string) (response string, err error)

Step takes a string provided from a server (or just an empty string for the very first conversation step) and attempts to move the authentication conversation forward. It returns a string to be sent to the server or an error if the server message is invalid. Calling Step after a conversation completes is also an error.

func (*ClientConversation) Valid

func (cc *ClientConversation) Valid() bool

Valid returns true if the conversation successfully authenticated with the server, including counter-validation that the server actually has the user's stored credentials.

type CredentialLookup

type CredentialLookup func(string) (StoredCredentials, error)

CredentialLookup is a callback to provide StoredCredentials for a given username. This is used to configure Server objects.

NOTE: these are specific to a given hash function. The callback provided to a Server with a given hash function must provide the corresponding StoredCredentials.

type HashGeneratorFcn

type HashGeneratorFcn func() hash.Hash

HashGeneratorFcn abstracts a factory function that returns a hash.Hash value to be used for SCRAM operations. Generally, one would use the provided package variables, `scram.SHA1` and `scram.SHA256`, for the most common forms of SCRAM.

var SHA1 HashGeneratorFcn = func() hash.Hash { return sha1.New() }

SHA1 is a function that returns a crypto/sha1 hasher and should be used to create Client objects configured for SHA-1 hashing.

var SHA256 HashGeneratorFcn = func() hash.Hash { return sha256.New() }

SHA256 is a function that returns a crypto/sha256 hasher and should be used to create Client objects configured for SHA-256 hashing.

var SHA512 HashGeneratorFcn = func() hash.Hash { return sha512.New() }

SHA512 is a function that returns a crypto/sha512 hasher and should be used to create Client objects configured for SHA-512 hashing.

func (HashGeneratorFcn) NewClient

func (f HashGeneratorFcn) NewClient(username, password, authzID string) (*Client, error)

NewClient constructs a SCRAM client component based on a given hash.Hash factory receiver. This constructor will normalize the username, password and authzID via the SASLprep algorithm, as recommended by RFC-5802. If SASLprep fails, the method returns an error.

func (HashGeneratorFcn) NewClientUnprepped

func (f HashGeneratorFcn) NewClientUnprepped(username, password, authzID string) (*Client, error)

NewClientUnprepped acts like NewClient, except none of the arguments will be normalized via SASLprep. This is not generally recommended, but is provided for users that may have custom normalization needs.

func (HashGeneratorFcn) NewServer

func (f HashGeneratorFcn) NewServer(cl CredentialLookup) (*Server, error)

NewServer constructs a SCRAM server component based on a given hash.Hash factory receiver. To be maximally generic, it uses dependency injection to handle credential lookup, which is the process of turning a username string into a struct with stored credentials for authentication.

type KeyFactors

type KeyFactors struct {
	Salt  string
	Iters int
}

KeyFactors represent the two server-provided factors needed to compute client credentials for authentication. Salt is decoded bytes (i.e. not base64), but in string form so that KeyFactors can be used as a map key for cached credentials.

type NonceGeneratorFcn

type NonceGeneratorFcn func() string

NonceGeneratorFcn defines a function that returns a string of high-quality random printable ASCII characters EXCLUDING the comma (',') character. The default nonce generator provides Base64 encoding of 24 bytes from crypto/rand.

type Server

type Server struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

Server implements the server side of SCRAM authentication. It holds configuration values needed to initialize new server-side conversations. Generally, this can be persistent within an application.

func (*Server) NewConversation

func (s *Server) NewConversation() *ServerConversation

NewConversation constructs a server-side authentication conversation. Conversations cannot be reused, so this must be called for each new authentication attempt.

func (*Server) NewConversationWithChannelBinding added in v1.2.0

func (s *Server) NewConversationWithChannelBinding(cb ChannelBinding) *ServerConversation

NewConversationWithChannelBinding constructs a server-side authentication conversation with channel binding for SCRAM-PLUS authentication.

This signals that the server advertised PLUS mechanism variants (e.g., SCRAM-SHA-256-PLUS) during SASL negotiation, but channel binding is NOT required. Clients may authenticate using either the base mechanism (e.g., SCRAM-SHA-256) or the PLUS variant (e.g., SCRAM-SHA-256-PLUS).

The server will:

  • Accept clients without channel binding support (using "n" flag)
  • Accept clients with matching channel binding (using "p" flag)
  • Reject downgrade attacks (clients using "y" flag when PLUS was advertised)

Channel binding is connection-specific, so a new conversation should be created for each connection being authenticated. Conversations cannot be reused, so this must be called for each new authentication attempt.

func (*Server) NewConversationWithChannelBindingRequired added in v1.2.0

func (s *Server) NewConversationWithChannelBindingRequired(cb ChannelBinding) *ServerConversation

NewConversationWithChannelBindingRequired constructs a server-side authentication conversation with mandatory channel binding for SCRAM-PLUS authentication.

This signals that the server advertised ONLY SCRAM-PLUS mechanism variants (e.g., only SCRAM-SHA-256-PLUS, not the base SCRAM-SHA-256) during SASL negotiation. Channel binding is required for all authentication attempts.

The server will:

  • Accept only clients with matching channel binding (using "p" flag)
  • Reject clients without channel binding support (using "n" flag)
  • Reject downgrade attacks (clients using "y" flag when PLUS was advertised)

This is intended for high-security deployments that advertise only SCRAM-PLUS variants and want to enforce channel binding as mandatory.

Channel binding is connection-specific, so a new conversation should be created for each connection being authenticated. Conversations cannot be reused, so this must be called for each new authentication attempt.

func (*Server) WithNonceGenerator

func (s *Server) WithNonceGenerator(ng NonceGeneratorFcn) *Server

WithNonceGenerator replaces the default nonce generator (base64 encoding of 24 bytes from crypto/rand) with a custom generator. This is provided for testing or for users with custom nonce requirements.

type ServerConversation

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

ServerConversation implements the server-side of an authentication conversation with a client. A new conversation must be created for each authentication attempt.

func (*ServerConversation) AuthzID

func (sc *ServerConversation) AuthzID() string

AuthzID returns the (optional) client-provided authorization identity, if any. If one was not provided, it returns the empty string. This is valid to call if the first conversation Step() is successful.

func (*ServerConversation) Done

func (sc *ServerConversation) Done() bool

Done returns true if the conversation is completed or has errored.

func (*ServerConversation) Step

func (sc *ServerConversation) Step(challenge string) (response string, err error)

Step takes a string provided from a client and attempts to move the authentication conversation forward. It returns a string to be sent to the client or an error if the client message is invalid. Calling Step after a conversation completes is also an error.

func (*ServerConversation) Username

func (sc *ServerConversation) Username() string

Username returns the client-provided username. This is valid to call if the first conversation Step() is successful.

func (*ServerConversation) Valid

func (sc *ServerConversation) Valid() bool

Valid returns true if the conversation successfully authenticated the client.

type StoredCredentials

type StoredCredentials struct {
	KeyFactors
	StoredKey []byte
	ServerKey []byte
}

StoredCredentials are the values that a server must store for a given username to allow authentication. They include the salt and iteration count, plus the derived values to authenticate a client and for the server to authenticate itself back to the client.

NOTE: these are specific to a given hash function. To allow a user to authenticate with either SCRAM-SHA-1 or SCRAM-SHA-256, two sets of StoredCredentials must be created and stored, one for each hash function.

Jump to

Keyboard shortcuts

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