Description
I'd like to see a package that provides RFC 4422 Simple Authentication and Security Layer (SASL) support in the golang.org/x/
package tree (possibly as golang.org/x/crypto/sasl
).
This could potentially be used under the covers in the net/smtp
package in the future, and would be broadly useful for people implementing other protocols (IMAP, AMQP, IRC, XMPP, memcached, POP, etc.). It would provide a way for varoius packages to share implementations of SASL mechanisms and not introduce problems by always reinventing the wheel every time something needs a SCRAM-SHA-1
implementation.
The API I had in mind (and have an implementation of) is something like this:
// State represents the current state of a Mechanism's underlying state machine.
type State int8
const (
Initial State = iota
AuthTextSent
ResponseSent
ValidServerResponse
)
const (
// Bit is on if the remote client or server supports channel binding.
RemoteCB State = 1 << (iota + 3)
// Bit is on if the machine has errored.
Errored
// Bit is on if the machine is a server.
Receiving
)
// Mechanism represents a SASL mechanism.
// A Mechanism is stateless and may be shared between goroutines or Negotiators.
type Mechanism struct {
// The name of the mechanism (eg. `DIGEST-MD5` or `SCRAM-SHA-2`).
Name string
// These functions get called by a Negotiator.
// I suppose Mechanism could be an interface too, but I like the idea of having
// it contain these functions so that Step can enforce security constraints on
// the state machine as much as possible (eg. it can be the only thing that's
// allowed to mutate the internal state). The bool returned from Next indicates
// that we should expect more challenges (Step needs to be called again
// before auth can be completed). The cache return value is stored by a
// Negotiator and passed back in as the data parameter with the next invocation
// of Next so that mechanisms can pass state between their steps while still
// remaining stateless themselves.
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)
}
// 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.
type Negotiator interface {
// Step is responsible for advancing the state machine and using the
// underlying mechanism. It should base64 decode the challenge (using the
// standard base64 encoding) and base64 encode the response generated from the
// underlying mechanism before returning it.
Step(challenge []byte) (more bool, resp []byte, err error)
State() State
Config() Config
Nonce() []byte
Reset()
}
// NewClient creates a new SASL client that supports the given mechanisms.
func NewClient(m Mechanism, opts ...Option) Negotiator
// Config is a SASL client or server configuration.
type Config struct {
// The state of any TLS connections being used to negotiate SASL (for channel
// binding).
TLSState *tls.ConnectionState
// A list of mechanisms as advertised by the other side of a SASL negotiation.
RemoteMechanisms []string
// I don't like having these here because other things might need other
// credentials (PGP key to sign a challenge with, OAuth token, etc.) and
// Adding tons of extra stuff here isn't very flexible. Suggestions welcome.
Identity, Username, Password string
}
type Option func(*Config)
func Authz(identity string) Option {}
func ConnState(cs tls.ConnectionState) Option {}
func Credentials(username, password string) Option {}
func RemoteMechanisms(m ...string) Option {}
…
var (
Plain Mechanism = plain
// These are identical internally, just the Hash used is different. Maybe it would make
// more sense just to expose a `SCRAM(h hash.Hash) Mechanism`
// function? On the other hand, 99% of the time people will probably just want these 4
// since they're the only ones standardized.
ScramSha256Plus = scram("SCRAM-SHA-256-PLUS", sha256.New)
ScramSha256 = scram("SCRAM-SHA-256", sha256.New)
ScramSha1Plus = scram("SCRAM-SHA-1-PLUS", sha1.New)
ScramSha1 = scram("SCRAM-SHA-1", sha1.New)
)
EDIT: I pushed my initial, experimental, implementation with an API similar to this: https://godoc.org/mellium.im/sasl