Repository: go
Issue: proposal: add SASL package to subrepos
Labels: Proposal,Proposal-Hold,Proposal-Crypto

I'd like to see a package that provides [RFC 4422](https://tools.ietf.org/html/rfc4422) 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:

``` go
//  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