sasl

package module
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Dec 28, 2022 License: BSD-2-Clause Imports: 13 Imported by: 83

README

SASL

Issue Tracker Docs Chat License

A Go library implementing the Simple Authentication and Security Layer (SASL) as defined by RFC 4422.

License

The package may be used under the terms of the BSD 2-Clause License a copy of which may be found in the file LICENSE.md.

Documentation

Overview

Package sasl implements the Simple Authentication and Security Layer (SASL) as defined by RFC 4422.

Most users of this package will only need to create a Negotiator using NewClient or NewServer and call its Step method repeatedly. Authors implementing SASL mechanisms other than the builtin ones will want to create a Mechanism struct which will likely use the other methods on the Negotiator.

Be advised: This API is still unstable and is subject to change.

Example (PlainFailure)
package main

import (
	"fmt"

	"mellium.im/sasl"
)

func main() {
	const (
		username = "miranda"
		password = "pencil"
	)

	creds := sasl.Credentials(func() ([]byte, []byte, []byte) {
		return []byte(username), []byte(password), []byte{}
	})

	server := sasl.NewServer(sasl.Plain, func(n *sasl.Negotiator) bool {
		user, pass, ident := n.Credentials()
		// In a real auth system you might want to consider a constant time
		// comparison and this would probably involve hashing and a database lookup.
		if len(ident) == 0 && string(user) == username && string(pass) == password {
			fmt.Println("auth success!")
			return true
		}
		fmt.Println("auth failed!")
		return false
	}, creds)

	client := sasl.NewClient(sasl.Plain, sasl.Credentials(func() ([]byte, []byte, []byte) {
		// In a real auth system this would probably be user input.
		return []byte(username), []byte("password!"), []byte{}
	}))

	_, resp, err := client.Step(nil)
	if err != nil {
		fmt.Println(err)
		return
	}

	// Normally the response would come from the network, not from a client on the
	// same machine.
	_, _, err = server.Step(resp)
	if err != sasl.ErrAuthn {
		fmt.Println(err)
		return
	}

}
Output:

auth failed!
Example (PlainSuccess)
package main

import (
	"fmt"

	"mellium.im/sasl"
)

func main() {
	const (
		username = "miranda"
		password = "pencil"
	)

	creds := sasl.Credentials(func() ([]byte, []byte, []byte) {
		// In a real auth system this would probably be user input.
		return []byte(username), []byte(password), []byte{}
	})

	server := sasl.NewServer(sasl.Plain, func(n *sasl.Negotiator) bool {
		user, pass, ident := n.Credentials()
		// In a real auth system you might want to consider a constant time
		// comparison and this would probably involve hashing and a database lookup.
		if len(ident) == 0 && string(user) == username && string(pass) == password {
			fmt.Println("auth success!")
			return true
		}
		fmt.Println("auth failed!")
		return false
	}, creds)

	client := sasl.NewClient(sasl.Plain, creds)

	_, resp, err := client.Step(nil)
	if err != nil {
		fmt.Println(err)
		return
	}

	// Normally the response would come from the network, not from a client on the
	// same machine.
	_, _, err = server.Step(resp)
	if err != nil {
		fmt.Println(err)
		return
	}

}
Output:

auth success!
Example (XOAUTH2)
package main

import (
	"bytes"
	"fmt"

	"mellium.im/sasl"
)

// A custom SASL Mechanism that implements XOAUTH2:
// https://developers.google.com/gmail/xoauth2_protocol
var xoauth2 = sasl.Mechanism{
	Name: "XOAUTH2",
	Start: func(m *sasl.Negotiator) (bool, []byte, interface{}, error) {
		// Start is called only by clients and returns the client first message.

		username, password, _ := m.Credentials()

		payload := []byte(`user=`)
		payload = append(payload, username...)
		payload = append(payload, '\x01')
		payload = append(payload, []byte(`auth=Bearer `)...)
		payload = append(payload, password...)
		payload = append(payload, '\x01', '\x01')

		return false, payload, nil, nil
	},
	Next: func(m *sasl.Negotiator, challenge []byte, _ interface{}) (bool, []byte, interface{}, error) {
		// Next is called by both clients and servers and must be able to generate
		// and handle every challenge except for the client first message which is
		// generated (but not handled by) by Start.

		state := m.State()

		// If we're a client or a server that's past the AuthTextSent step, we
		// should never actually hit this step for the XOAUTH2 mechanism so return
		// an error.
		if state&sasl.Receiving != sasl.Receiving || state&sasl.StepMask != sasl.AuthTextSent {
			return false, nil, nil, sasl.ErrTooManySteps
		}

		parts := bytes.Split(challenge, []byte{1})
		if len(parts) != 3 {
			return false, nil, nil, sasl.ErrInvalidChallenge
		}
		user := bytes.TrimPrefix([]byte("user="), parts[0])
		if len(user) == len(parts[0]) {
			return false, nil, nil, sasl.ErrInvalidChallenge
		}
		pass := bytes.TrimPrefix([]byte("Auth=Bearer "), parts[1])
		if len(pass) == len(parts[1]) {
			return false, nil, nil, sasl.ErrInvalidChallenge
		}
		if len(parts[2]) > 0 {
			return false, nil, nil, sasl.ErrInvalidChallenge
		}

		if m.Permissions(sasl.Credentials(func() ([]byte, []byte, []byte) {
			return user, pass, nil
		})) {
			return false, nil, nil, nil
		}
		return false, nil, nil, sasl.ErrAuthn
	},
}

func main() {
	c := sasl.NewClient(
		xoauth2,
		sasl.Credentials(func() ([]byte, []byte, []byte) {
			return []byte("someuser@example.com"), []byte("vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg=="), []byte{}
		}),
	)

	// This is the first step and we haven't received any challenge from the
	// server yet.
	more, resp, _ := c.Step(nil)
	fmt.Printf("%v %s", more, bytes.Replace(resp, []byte{1}, []byte{' '}, -1))

}
Output:

false user=someuser@example.com auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidState     = errors.New("invalid state")
	ErrInvalidChallenge = errors.New("invalid or missing challenge")
	ErrAuthn            = errors.New("authentication error")
	ErrTooManySteps     = errors.New("step called too many times")
)

Define common errors used by SASL mechanisms and negotiators.

Functions

This section is empty.

Types

type Mechanism

type Mechanism struct {
	Name  string
	Start func(n *Negotiator) (more bool, resp []byte, cache interface{}, err error)
	Next  func(n *Negotiator, challenge []byte, data interface{}) (more bool, resp []byte, cache interface{}, err error)
}

Mechanism represents a SASL mechanism that can be used by a Client or Server to perform the actual negotiation. Base64 encoding the final challenges and responses should not be performed by the mechanism.

Mechanisms must be stateless and may be shared between goroutines. When a mechanism needs to store state between the different steps it can return anything that it needs to store and the value will be cached by the negotiator and passed in as the data parameter when the next challenge is received.

var (
	// Plain is a Mechanism that implements the PLAIN authentication mechanism
	// as defined by RFC 4616.
	Plain Mechanism = plain

	// ScramSha256Plus is a Mechanism that implements the SCRAM-SHA-256-PLUS
	// authentication mechanism defined in RFC 7677.
	// The only supported channel binding types are tls-unique as defined in RFC
	// 5929 and tls-exporter defined in RFC 9266.
	ScramSha256Plus Mechanism = scram("SCRAM-SHA-256-PLUS", sha256.New)

	// ScramSha256 is a Mechanism that implements the SCRAM-SHA-256
	// authentication mechanism defined in RFC 7677.
	ScramSha256 Mechanism = scram("SCRAM-SHA-256", sha256.New)

	// ScramSha1Plus is a Mechanism that implements the SCRAM-SHA-1-PLUS
	// authentication mechanism defined in RFC 5802.
	// The only supported channel binding types are tls-unique as defined in RFC
	// 5929 and tls-exporter defined in RFC 9266.
	ScramSha1Plus Mechanism = scram("SCRAM-SHA-1-PLUS", sha1.New)

	// ScramSha1 is a Mechanism that implements the SCRAM-SHA-1 authentication
	// mechanism defined in RFC 5802.
	ScramSha1 Mechanism = scram("SCRAM-SHA-1", sha1.New)
)

type Negotiator

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

A Negotiator represents a SASL client or server state machine that can attempt to negotiate auth. Negotiators should not be used from multiple goroutines, and must be reset between negotiation attempts.

func NewClient

func NewClient(m Mechanism, opts ...Option) *Negotiator

NewClient creates a new SASL Negotiator that supports creating authentication requests using the given mechanism.

func NewServer added in v0.1.0

func NewServer(m Mechanism, permissions func(*Negotiator) bool, opts ...Option) *Negotiator

NewServer creates a new SASL Negotiator that supports receiving authentication requests using the given mechanism. A nil permissions function is the same as a function that always returns false.

func (*Negotiator) Credentials added in v0.1.0

func (c *Negotiator) Credentials() (username, password, identity []byte)

Credentials returns a username, and password for authentication and optional identity for authorization.

func (*Negotiator) Nonce

func (c *Negotiator) Nonce() []byte

Nonce returns a unique nonce that is reset for each negotiation attempt. It is used by SASL Mechanisms and should generally not be called directly.

func (*Negotiator) Permissions added in v0.1.0

func (c *Negotiator) Permissions(opts ...Option) bool

Permissions is the callback used by the server to authenticate the user.

func (*Negotiator) RemoteMechanisms added in v0.1.0

func (c *Negotiator) RemoteMechanisms() []string

RemoteMechanisms is a list of mechanisms as advertised by the other side of a SASL negotiation.

func (*Negotiator) Reset

func (c *Negotiator) Reset()

Reset resets the state machine to its initial state so that it can be reused in another SASL exchange.

func (*Negotiator) State

func (c *Negotiator) State() State

State returns the internal state of the SASL state machine.

func (*Negotiator) Step

func (c *Negotiator) Step(challenge []byte) (more bool, resp []byte, err error)

Step attempts to transition the state machine to its next state. If Step is called after a previous invocation generates an error (and the state machine has not been reset to its initial state), Step panics.

func (*Negotiator) TLSState added in v0.1.0

func (c *Negotiator) TLSState() *tls.ConnectionState

TLSState is the state of any TLS connections being used to negotiate SASL (it can be used for channel binding).

type Option

type Option func(*Negotiator)

An Option represents an input to a SASL state machine.

func Credentials

func Credentials(f func() (Username, Password, Identity []byte)) Option

Credentials provides the negotiator with a username and password to authenticate with and (optionally) an authorization identity. Identity will normally be left empty to act as the username. The Credentials function is called lazily and may be called multiple times by the mechanism. It is not memoized by the negotiator.

func RemoteMechanisms

func RemoteMechanisms(m ...string) Option

RemoteMechanisms sets a list of mechanisms supported by the remote client or server with which the state machine will be negotiating. It is used to determine if the server supports channel binding.

func TLSState added in v0.1.0

func TLSState(cs tls.ConnectionState) Option

TLSState lets the state machine negotiate channel binding with a TLS session if supported by the underlying mechanism.

type State

type State uint8

State represents the current state of a Negotiator. The first two bits represent the actual state of the state machine and the last 3 bits are a bitmask that define the machines behavior. The remaining bits should not be used.

const (
	Initial State = iota
	AuthTextSent
	ResponseSent
	ValidServerResponse

	// Bitmask used for extracting the step from the state byte.
	StepMask = 0x3
)

The current step of the Server or Client (represented by the first two bits of the state byte).

const (
	// RemoteCB bit is on if the remote client or server supports channel binding.
	RemoteCB State = 1 << (iota + 3)

	// Errored bit is on if the machine has errored.
	Errored

	// Receiving bit is on if the machine is a server.
	Receiving
)

Notes

Bugs

  • We need a way to cache the SCRAM client and server key calculations.

Directories

Path Synopsis
internal
gsasl
Package gsasl uses cgo to facilitate integration testing against libgsasl.
Package gsasl uses cgo to facilitate integration testing against libgsasl.

Jump to

Keyboard shortcuts

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