2023-02-05 14:30:14 +03:00
// Package scram implements the SCRAM-SHA-* SASL authentication mechanism, RFC 7677 and RFC 5802.
2023-01-30 16:27:06 +03:00
//
2023-02-05 14:30:14 +03:00
// SCRAM-SHA-256 and SCRAM-SHA-1 allow a client to authenticate to a server using a
// password without handing plaintext password over to the server. The client also
2023-12-12 17:47:26 +03:00
// verifies the server knows (a derivative of) the password. Both the client and
// server side are implemented.
2023-01-30 16:27:06 +03:00
package scram
// todo: test with messages that contains extensions
// todo: some tests for the parser
// todo: figure out how invalid parameters etc should be handled. just abort? perhaps mostly a problem for imap.
import (
"bytes"
"crypto/hmac"
cryptorand "crypto/rand"
2023-12-24 01:07:21 +03:00
"crypto/tls"
2023-01-30 16:27:06 +03:00
"encoding/base64"
"errors"
"fmt"
2023-02-05 14:30:14 +03:00
"hash"
2023-01-30 16:27:06 +03:00
"strings"
"golang.org/x/crypto/pbkdf2"
2024-03-09 01:29:15 +03:00
"golang.org/x/text/secure/precis"
2023-01-30 16:27:06 +03:00
"golang.org/x/text/unicode/norm"
)
// Errors at scram protocol level. Can be exchanged between client and server.
var (
ErrInvalidEncoding Error = "invalid-encoding"
ErrExtensionsNotSupported Error = "extensions-not-supported"
ErrInvalidProof Error = "invalid-proof"
ErrChannelBindingsDontMatch Error = "channel-bindings-dont-match"
ErrServerDoesSupportChannelBinding Error = "server-does-support-channel-binding"
ErrChannelBindingNotSupported Error = "channel-binding-not-supported"
ErrUnsupportedChannelBindingType Error = "unsupported-channel-binding-type"
ErrUnknownUser Error = "unknown-user"
ErrNoResources Error = "no-resources"
ErrOtherError Error = "other-error"
)
var scramErrors = makeErrors ( )
func makeErrors ( ) map [ string ] Error {
l := [ ] Error {
ErrInvalidEncoding ,
ErrExtensionsNotSupported ,
ErrInvalidProof ,
ErrChannelBindingsDontMatch ,
ErrServerDoesSupportChannelBinding ,
ErrChannelBindingNotSupported ,
ErrUnsupportedChannelBindingType ,
ErrUnknownUser ,
ErrNoResources ,
ErrOtherError ,
}
m := map [ string ] Error { }
for _ , e := range l {
m [ string ( e ) ] = e
}
return m
}
var (
ErrNorm = errors . New ( "parameter not unicode normalized" ) // E.g. if client sends non-normalized username or authzid.
ErrUnsafe = errors . New ( "unsafe parameter" ) // E.g. salt, nonce too short, or too few iterations.
ErrProtocol = errors . New ( "protocol error" ) // E.g. server responded with a nonce not prefixed by the client nonce.
)
type Error string
func ( e Error ) Error ( ) string {
return string ( e )
}
// MakeRandom returns a cryptographically random buffer for use as salt or as
// nonce.
func MakeRandom ( ) [ ] byte {
buf := make ( [ ] byte , 12 )
_ , err := cryptorand . Read ( buf )
if err != nil {
panic ( "generate random" )
}
return buf
}
2024-03-09 01:29:15 +03:00
// Cleanup password with precis, like remote should have done. If the password
// appears invalid, we'll return the original, there is a chance the server also
// doesn't enforce requirements and accepts it. ../rfc/8265:679
func precisPassword ( password string ) string {
pw , err := precis . OpaqueString . String ( password )
if err != nil {
return password
}
return pw
}
2023-01-30 16:27:06 +03:00
// SaltPassword returns a salted password.
2023-02-05 14:30:14 +03:00
func SaltPassword ( h func ( ) hash . Hash , password string , salt [ ] byte , iterations int ) [ ] byte {
2024-03-09 01:29:15 +03:00
password = precisPassword ( password )
2023-02-05 14:30:14 +03:00
return pbkdf2 . Key ( [ ] byte ( password ) , salt , iterations , h ( ) . Size ( ) , h )
2023-01-30 16:27:06 +03:00
}
2023-12-12 17:47:26 +03:00
// hmac0 returns the hmac with key over msg.
func hmac0 ( h func ( ) hash . Hash , key [ ] byte , msg string ) [ ] byte {
2023-02-05 14:30:14 +03:00
mac := hmac . New ( h , key )
2023-01-30 16:27:06 +03:00
mac . Write ( [ ] byte ( msg ) )
return mac . Sum ( nil )
}
func xor ( a , b [ ] byte ) {
for i := range a {
a [ i ] ^ = b [ i ]
}
}
2023-12-24 01:07:21 +03:00
func channelBindData ( cs * tls . ConnectionState ) ( [ ] byte , error ) {
if cs . Version <= tls . VersionTLS12 {
if cs . TLSUnique == nil {
return nil , fmt . Errorf ( "no channel binding data available" )
}
return cs . TLSUnique , nil
}
// "tls-exporter", ../rfc/9266:95
// Since TLS 1.3, a zero-length and absent context have the same behaviour. ../rfc/8446:5385 ../rfc/8446:5405
// This is different from TLS 1.2 and earlier. ../rfc/5705:206 ../rfc/5705:245
return cs . ExportKeyingMaterial ( "EXPORTER-Channel-Binding" , [ ] byte { } , 32 )
}
2023-02-05 14:30:14 +03:00
// Server represents the server-side of a SCRAM-SHA-* authentication.
2023-01-30 16:27:06 +03:00
type Server struct {
Authentication string // Username for authentication, "authc". Always set and non-empty.
Authorization string // If set, role of user to assume after authentication, "authz".
2023-02-05 14:30:14 +03:00
h func ( ) hash . Hash // sha1.New or sha256.New
2023-01-30 16:27:06 +03:00
// Messages used in hash calculations.
clientFirstBare string
serverFirst string
clientFinalWithoutProof string
gs2header string
clientNonce string // Client-part of the nonce.
serverNonceOverride string // If set, server does not generate random nonce, but uses this. For tests with the test vector.
nonce string // Full client + server nonce.
2023-12-24 01:07:21 +03:00
channelBinding [ ] byte
2023-01-30 16:27:06 +03:00
}
// NewServer returns a server given the first SCRAM message from a client.
//
2023-12-24 01:07:21 +03:00
// If cs is set, the PLUS variant can be negotiated, binding the authentication
// exchange to the TLS channel (preventing MitM attempts). If a client
// indicates it supports the PLUS variant, but thinks the server does not, the
// authentication attempt will fail.
//
// If channelBindingRequired is set, the client has indicated it will do channel
// binding and not doing so will cause the authentication to fail.
//
2023-01-30 16:27:06 +03:00
// The sequence for data and calls on a server:
//
// - Read initial data from client, call NewServer (this call), then ServerFirst and write to the client.
// - Read response from client, call Finish or FinishFinal and write the resulting string.
2023-12-24 01:07:21 +03:00
func NewServer ( h func ( ) hash . Hash , clientFirst [ ] byte , cs * tls . ConnectionState , channelBindingRequired bool ) ( server * Server , rerr error ) {
2023-01-30 16:27:06 +03:00
p := newParser ( clientFirst )
defer p . recover ( & rerr )
2023-02-05 14:30:14 +03:00
server = & Server { h : h }
2023-01-30 16:27:06 +03:00
// ../rfc/5802:949 ../rfc/5802:910
gs2cbindFlag := p . xbyte ( )
switch gs2cbindFlag {
2023-12-24 01:07:21 +03:00
case 'n' :
// Client does not support channel binding.
if channelBindingRequired {
p . xerrorf ( "channel binding is required when specifying scram plus: %w" , ErrChannelBindingsDontMatch )
}
case 'y' :
// Client supports channel binding but thinks we as server do not.
p . xerrorf ( "gs2 channel bind flag is y, client believes server does not support channel binding: %w" , ErrServerDoesSupportChannelBinding )
2023-01-30 16:27:06 +03:00
case 'p' :
2023-12-24 01:07:21 +03:00
// Use channel binding.
// It seems a cyrus-sasl client tells a server it is using the bare (non-PLUS)
// scram authentication mechanism, but then does use channel binding. It seems to
// use the server announcement of the plus variant only to learn the server
// supports channel binding.
p . xtake ( "=" )
cbname := p . xcbname ( )
// Assume the channel binding name is case-sensitive, and lower-case as used in
// examples. The ABNF rule accepts both lower and upper case. But the ABNF for
// attribute names also allows that, while the text claims they are case
// sensitive... ../rfc/5802:547
switch cbname {
case "tls-unique" :
if cs == nil {
p . xerrorf ( "no tls connection: %w" , ErrChannelBindingsDontMatch )
} else if cs . Version >= tls . VersionTLS13 {
// ../rfc/9266:122
p . xerrorf ( "tls-unique not defined for tls 1.3 and later, use tls-exporter: %w" , ErrChannelBindingsDontMatch )
} else if cs . TLSUnique == nil {
// As noted in the crypto/tls documentation.
p . xerrorf ( "no tls-unique channel binding value for this tls connection, possibly due to missing extended master key support and/or resumed connection: %w" , ErrChannelBindingsDontMatch )
}
case "tls-exporter" :
if cs == nil {
p . xerrorf ( "no tls connection: %w" , ErrChannelBindingsDontMatch )
} else if cs . Version < tls . VersionTLS13 {
// Using tls-exporter with pre-1.3 TLS would require more precautions. Perhaps later.
// ../rfc/9266:201
p . xerrorf ( "tls-exporter with tls before 1.3 not implemented, use tls-unique: %w" , ErrChannelBindingsDontMatch )
}
default :
p . xerrorf ( "unknown parameter p %s: %w" , cbname , ErrUnsupportedChannelBindingType )
}
cb , err := channelBindData ( cs )
if err != nil {
// We can pass back the error, it should never contain sensitive data, and only
// happen due to incorrect calling or a TLS config that is currently impossible
// (renegotiation enabled).
p . xerrorf ( "error fetching channel binding data: %v: %w" , err , ErrOtherError )
}
server . channelBinding = cb
default :
p . xerrorf ( "unrecognized gs2 channel bind flag" )
2023-01-30 16:27:06 +03:00
}
p . xtake ( "," )
if ! p . take ( "," ) {
server . Authorization = p . xauthzid ( )
if norm . NFC . String ( server . Authorization ) != server . Authorization {
return nil , fmt . Errorf ( "%w: authzid" , ErrNorm )
}
p . xtake ( "," )
}
server . gs2header = p . s [ : p . o ]
server . clientFirstBare = p . s [ p . o : ]
2023-12-24 01:07:21 +03:00
// ../rfc/5802:632
// ../rfc/5802:946
2023-01-30 16:27:06 +03:00
if p . take ( "m=" ) {
2023-12-24 01:07:21 +03:00
p . xerrorf ( "unexpected mandatory extension: %w" , ErrExtensionsNotSupported ) // ../rfc/5802:973
2023-01-30 16:27:06 +03:00
}
server . Authentication = p . xusername ( )
if norm . NFC . String ( server . Authentication ) != server . Authentication {
return nil , fmt . Errorf ( "%w: username" , ErrNorm )
}
p . xtake ( "," )
server . clientNonce = p . xnonce ( )
if len ( server . clientNonce ) < 8 {
return nil , fmt . Errorf ( "%w: client nonce too short" , ErrUnsafe )
}
// Extensions, we don't recognize them.
for p . take ( "," ) {
p . xattrval ( )
}
p . xempty ( )
return server , nil
}
// ServerFirst returns the string to send back to the client. To be called after NewServer.
func ( s * Server ) ServerFirst ( iterations int , salt [ ] byte ) ( string , error ) {
// ../rfc/5802:959
serverNonce := s . serverNonceOverride
if serverNonce == "" {
serverNonce = base64 . StdEncoding . EncodeToString ( MakeRandom ( ) )
}
s . nonce = s . clientNonce + serverNonce
s . serverFirst = fmt . Sprintf ( "r=%s,s=%s,i=%d" , s . nonce , base64 . StdEncoding . EncodeToString ( salt ) , iterations )
return s . serverFirst , nil
}
// Finish takes the final client message, and the salted password (probably
// from server storage), verifies the client, and returns a message to return
// to the client. If err is nil, authentication was successful. If the
// authorization requested is not acceptable, the server should call
// FinishError instead.
func ( s * Server ) Finish ( clientFinal [ ] byte , saltedPassword [ ] byte ) ( serverFinal string , rerr error ) {
p := newParser ( clientFinal )
defer p . recover ( & rerr )
2023-12-24 01:07:21 +03:00
// If there is any channel binding, and it doesn't match, this may be a
// MitM-attack. If the MitM would replace the channel binding, the signature
// calculated below would not match.
2023-01-30 16:27:06 +03:00
cbind := p . xchannelBinding ( )
2023-12-24 01:07:21 +03:00
cbindExp := append ( [ ] byte ( s . gs2header ) , s . channelBinding ... )
if ! bytes . Equal ( cbind , cbindExp ) {
2023-01-30 16:27:06 +03:00
return "e=" + string ( ErrChannelBindingsDontMatch ) , ErrChannelBindingsDontMatch
}
p . xtake ( "," )
nonce := p . xnonce ( )
if nonce != s . nonce {
return "e=" + string ( ErrInvalidProof ) , ErrInvalidProof
}
for ! p . peek ( ",p=" ) {
p . xtake ( "," )
p . xattrval ( ) // Ignored.
}
s . clientFinalWithoutProof = p . s [ : p . o ]
p . xtake ( "," )
proof := p . xproof ( )
p . xempty ( )
2023-12-24 01:07:21 +03:00
authMsg := s . clientFirstBare + "," + s . serverFirst + "," + s . clientFinalWithoutProof
2023-01-30 16:27:06 +03:00
2023-12-12 17:47:26 +03:00
clientKey := hmac0 ( s . h , saltedPassword , "Client Key" )
2023-02-05 14:30:14 +03:00
h := s . h ( )
h . Write ( clientKey )
storedKey := h . Sum ( nil )
2023-01-30 16:27:06 +03:00
2023-12-24 01:07:21 +03:00
clientSig := hmac0 ( s . h , storedKey , authMsg )
2023-01-30 16:27:06 +03:00
xor ( clientSig , clientKey ) // Now clientProof.
if ! bytes . Equal ( clientSig , proof ) {
return "e=" + string ( ErrInvalidProof ) , ErrInvalidProof
}
2023-12-12 17:47:26 +03:00
serverKey := hmac0 ( s . h , saltedPassword , "Server Key" )
2023-12-24 01:07:21 +03:00
serverSig := hmac0 ( s . h , serverKey , authMsg )
2023-01-30 16:27:06 +03:00
return fmt . Sprintf ( "v=%s" , base64 . StdEncoding . EncodeToString ( serverSig ) ) , nil
}
// FinishError returns an error message to write to the client for the final
// server message.
func ( s * Server ) FinishError ( err Error ) string {
return "e=" + string ( err )
}
2023-02-05 14:30:14 +03:00
// Client represents the client-side of a SCRAM-SHA-* authentication.
2023-01-30 16:27:06 +03:00
type Client struct {
authc string
authz string
2023-12-24 01:07:21 +03:00
h func ( ) hash . Hash // sha1.New or sha256.New
noServerPlus bool // Server did not announce support for PLUS-variant.
cs * tls . ConnectionState // If set, use PLUS-variant.
2023-02-05 14:30:14 +03:00
2023-01-30 16:27:06 +03:00
// Messages used in hash calculations.
clientFirstBare string
serverFirst string
clientFinalWithoutProof string
authMessage string
2023-12-24 01:07:21 +03:00
gs2header string
clientNonce string
nonce string // Full client + server nonce.
saltedPassword [ ] byte
channelBindData [ ] byte // For PLUS-variant.
2023-01-30 16:27:06 +03:00
}
// NewClient returns a client for authentication authc, optionally for
2023-02-05 14:30:14 +03:00
// authorization with role authz, for the hash (sha1.New or sha256.New).
2023-01-30 16:27:06 +03:00
//
2023-12-24 01:07:21 +03:00
// If noServerPlus is true, the client would like to have used the PLUS-variant,
// that binds the authentication attempt to the TLS connection, but the client did
// not see support for the PLUS variant announced by the server. Used during
// negotiation to detect possible MitM attempt.
//
// If cs is not nil, the SCRAM PLUS-variant is negotiated, with channel binding to
// the unique TLS connection, either using "tls-exporter" for TLS 1.3 and later, or
// "tls-unique" otherwise.
//
// If cs is nil, no channel binding is done. If noServerPlus is also false, the
// client is configured to not attempt/"support" the PLUS-variant, ensuring servers
// that do support the PLUS-variant do not abort the connection.
//
2023-01-30 16:27:06 +03:00
// The sequence for data and calls on a client:
//
// - ClientFirst, write result to server.
// - Read response from server, feed to ServerFirst, write response to server.
// - Read response from server, feed to ServerFinal.
2023-12-24 01:07:21 +03:00
func NewClient ( h func ( ) hash . Hash , authc , authz string , noServerPlus bool , cs * tls . ConnectionState ) * Client {
2023-01-30 16:27:06 +03:00
authc = norm . NFC . String ( authc )
authz = norm . NFC . String ( authz )
2023-12-24 01:07:21 +03:00
return & Client { authc : authc , authz : authz , h : h , noServerPlus : noServerPlus , cs : cs }
2023-01-30 16:27:06 +03:00
}
// ClientFirst returns the first client message to write to the server.
// No channel binding is done/supported.
// A random nonce is generated.
func ( c * Client ) ClientFirst ( ) ( clientFirst string , rerr error ) {
2023-12-24 01:07:21 +03:00
if c . noServerPlus && c . cs != nil {
return "" , fmt . Errorf ( "cannot set both claim channel binding is not supported, and use channel binding" )
}
// The first byte of the gs2header indicates if/how channel binding should be used.
// ../rfc/5802:903
if c . cs != nil {
if c . cs . Version >= tls . VersionTLS13 {
c . gs2header = "p=tls-exporter"
} else {
c . gs2header = "p=tls-unique"
}
cbdata , err := channelBindData ( c . cs )
if err != nil {
return "" , fmt . Errorf ( "get channel binding data: %v" , err )
}
c . channelBindData = cbdata
} else if c . noServerPlus {
// We support it, but we think server does not. If server does support it, we may
// have been downgraded, and the server will tell us.
c . gs2header = "y"
} else {
// We don't want to do channel binding.
c . gs2header = "n"
}
c . gs2header += fmt . Sprintf ( ",%s," , saslname ( c . authz ) )
2023-01-30 16:27:06 +03:00
if c . clientNonce == "" {
c . clientNonce = base64 . StdEncoding . EncodeToString ( MakeRandom ( ) )
}
c . clientFirstBare = fmt . Sprintf ( "n=%s,r=%s" , saslname ( c . authc ) , c . clientNonce )
return c . gs2header + c . clientFirstBare , nil
}
// ServerFirst processes the first response message from the server. The
// provided nonce, salt and iterations are checked. If valid, a final client
// message is calculated and returned. This message must be written to the
// server. It includes proof that the client knows the password.
func ( c * Client ) ServerFirst ( serverFirst [ ] byte , password string ) ( clientFinal string , rerr error ) {
c . serverFirst = string ( serverFirst )
p := newParser ( serverFirst )
defer p . recover ( & rerr )
2023-12-24 01:07:21 +03:00
// ../rfc/5802:632
2023-01-30 16:27:06 +03:00
// ../rfc/5802:959
if p . take ( "m=" ) {
2023-12-24 01:07:21 +03:00
p . xerrorf ( "unsupported mandatory extension: %w" , ErrExtensionsNotSupported ) // ../rfc/5802:973
2023-01-30 16:27:06 +03:00
}
c . nonce = p . xnonce ( )
p . xtake ( "," )
salt := p . xsalt ( )
p . xtake ( "," )
iterations := p . xiterations ( )
// We ignore extensions that we don't know about.
for p . take ( "," ) {
p . xattrval ( )
}
p . xempty ( )
if ! strings . HasPrefix ( c . nonce , c . clientNonce ) {
return "" , fmt . Errorf ( "%w: server dropped our nonce" , ErrProtocol )
}
if len ( c . nonce ) - len ( c . clientNonce ) < 8 {
return "" , fmt . Errorf ( "%w: server nonce too short" , ErrUnsafe )
}
if len ( salt ) < 8 {
return "" , fmt . Errorf ( "%w: salt too short" , ErrUnsafe )
}
if iterations < 2048 {
return "" , fmt . Errorf ( "%w: too few iterations" , ErrUnsafe )
}
2023-12-24 01:07:21 +03:00
// We send our channel binding data if present. If the server has different values,
// we'll get an error. If any MitM would try to modify the channel binding data,
// the server cannot verify our signature and will fail the attempt.
// ../rfc/5802:925 ../rfc/5802:1015
cbindInput := append ( [ ] byte ( c . gs2header ) , c . channelBindData ... )
c . clientFinalWithoutProof = fmt . Sprintf ( "c=%s,r=%s" , base64 . StdEncoding . EncodeToString ( cbindInput ) , c . nonce )
2023-01-30 16:27:06 +03:00
c . authMessage = c . clientFirstBare + "," + c . serverFirst + "," + c . clientFinalWithoutProof
2023-02-05 14:30:14 +03:00
c . saltedPassword = SaltPassword ( c . h , password , salt , iterations )
2023-12-12 17:47:26 +03:00
clientKey := hmac0 ( c . h , c . saltedPassword , "Client Key" )
2023-02-05 14:30:14 +03:00
h := c . h ( )
h . Write ( clientKey )
storedKey := h . Sum ( nil )
2023-12-12 17:47:26 +03:00
clientSig := hmac0 ( c . h , storedKey , c . authMessage )
2023-01-30 16:27:06 +03:00
xor ( clientSig , clientKey ) // Now clientProof.
clientProof := clientSig
r := c . clientFinalWithoutProof + ",p=" + base64 . StdEncoding . EncodeToString ( clientProof )
return r , nil
}
// ServerFinal processes the final message from the server, verifying that the
// server knows the password.
func ( c * Client ) ServerFinal ( serverFinal [ ] byte ) ( rerr error ) {
p := newParser ( serverFinal )
defer p . recover ( & rerr )
if p . take ( "e=" ) {
errstr := p . xvalue ( )
var err error = scramErrors [ errstr ]
if err == Error ( "" ) {
err = errors . New ( errstr )
}
return fmt . Errorf ( "error from server: %w" , err )
}
p . xtake ( "v=" )
verifier := p . xbase64 ( )
2023-12-12 17:47:26 +03:00
serverKey := hmac0 ( c . h , c . saltedPassword , "Server Key" )
serverSig := hmac0 ( c . h , serverKey , c . authMessage )
2023-01-30 16:27:06 +03:00
if ! bytes . Equal ( verifier , serverSig ) {
return fmt . Errorf ( "incorrect server signature" )
}
return nil
}
// Convert "," to =2C and "=" to =3D.
func saslname ( s string ) string {
var r string
for _ , c := range s {
if c == ',' {
r += "=2C"
} else if c == '=' {
r += "=3D"
} else {
r += string ( c )
}
}
return r
}