Skip to content

Commit

Permalink
Merge pull request #399 from nats-io/nkeys
Browse files Browse the repository at this point in the history
Nkey support
  • Loading branch information
derekcollison authored Oct 29, 2018
2 parents 208e287 + 4515025 commit 733a0cc
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 14 deletions.
77 changes: 63 additions & 14 deletions nats.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -94,6 +95,8 @@ var (
ErrInvalidContext = errors.New("nats: invalid context")
ErrNoEchoNotSupported = errors.New("nats: no echo option not supported by this server")
ErrClientIDNotSupported = errors.New("nats: client ID not supported by this server")
ErrNkeyButNoSigCB = errors.New("nats: Nkey defined without a signature handler")
ErrNkeysNoSupported = errors.New("nats: Nkeys not supported by the server")
ErrStaleConnection = errors.New("nats: " + STALE_CONNECTION)
)

Expand Down Expand Up @@ -138,6 +141,11 @@ type ConnHandler func(*Conn)
// while processing inbound messages.
type ErrHandler func(*Conn, *Subscription, error)

// SignatureHandler is used to sign a nonce from the server while
// authenticating with nkeys. The user should sign the nonce and
// return the base64 encoded signature.
type SignatureHandler func([]byte) []byte

// asyncCB is used to preserve order for async callbacks.
type asyncCB struct {
f func()
Expand Down Expand Up @@ -261,6 +269,14 @@ type Options struct {
// dictated by PendingLimits()
SubChanLen int

// Nkey sets the public nkey that will be used to authenticate
// when connecting to the server
Nkey string

// SignatureCB designates the function used to sign the nonce
// presented from the server.
SignatureCB SignatureHandler

// User sets the username to be used when connecting to the server.
User string

Expand Down Expand Up @@ -430,6 +446,7 @@ type serverInfo struct {
ConnectURLs []string `json:"connect_urls,omitempty"`
Proto int `json:"proto,omitempty"`
CID uint64 `json:"client_id,omitempty"`
Nonce string `json:"nonce,omitempty"`
}

const (
Expand All @@ -442,17 +459,19 @@ const (
)

type connectInfo struct {
Verbose bool `json:"verbose"`
Pedantic bool `json:"pedantic"`
User string `json:"user,omitempty"`
Pass string `json:"pass,omitempty"`
Token string `json:"auth_token,omitempty"`
TLS bool `json:"tls_required"`
Name string `json:"name"`
Lang string `json:"lang"`
Version string `json:"version"`
Protocol int `json:"protocol"`
Echo bool `json:"echo"`
Verbose bool `json:"verbose"`
Pedantic bool `json:"pedantic"`
Nkey string `json:"nkey,omitempty"`
Signature string `json:"sig,omitempty"`
User string `json:"user,omitempty"`
Pass string `json:"pass,omitempty"`
Token string `json:"auth_token,omitempty"`
TLS bool `json:"tls_required"`
Name string `json:"name"`
Lang string `json:"lang"`
Version string `json:"version"`
Protocol int `json:"protocol"`
Echo bool `json:"echo"`
}

// MsgHandler is a callback function that processes messages delivered to
Expand Down Expand Up @@ -680,6 +699,19 @@ func Token(token string) Option {
}
}

// Nkey will set the public Nkey and the signature callback to
// sign the server nonce.
func Nkey(pubKey string, sigCB SignatureHandler) Option {
return func(o *Options) error {
o.Nkey = pubKey
o.SignatureCB = sigCB
if pubKey != "" && sigCB == nil {
return ErrNkeyButNoSigCB
}
return nil
}
}

// Dialer is an Option to set the dialer which will be used when
// attempting to establish a connection.
// DEPRECATED: Should use CustomDialer instead.
Expand Down Expand Up @@ -791,6 +823,11 @@ func (o Options) Connect() (*Conn, error) {
nc.Opts.Timeout = DefaultTimeout
}

// Check if we have an nkey but no signature callback defined.
if nc.Opts.Nkey != "" && nc.Opts.SignatureCB == nil {
return nil, ErrNkeyButNoSigCB
}

// Allow custom Dialer for connecting using DialTimeout by default
if nc.Opts.Dialer == nil {
nc.Opts.Dialer = &net.Dialer{
Expand Down Expand Up @@ -1237,6 +1274,10 @@ func (nc *Conn) processExpectedInfo() error {
return err
}

if nc.Opts.Nkey != "" && nc.info.Nonce == "" {
return ErrNkeysNoSupported
}

return nc.checkForSecure()
}

Expand All @@ -1253,7 +1294,7 @@ func (nc *Conn) sendProto(proto string) {
// applicable. The lock is assumed to be held upon entering.
func (nc *Conn) connectProto() (string, error) {
o := nc.Opts
var user, pass, token string
var nkey, sig, user, pass, token string
u := nc.url.User
if u != nil {
// if no password, assume username is authToken
Expand All @@ -1268,10 +1309,17 @@ func (nc *Conn) connectProto() (string, error) {
user = nc.Opts.User
pass = nc.Opts.Password
token = nc.Opts.Token
nkey = nc.Opts.Nkey
}

if nkey != "" {
sigraw := o.SignatureCB([]byte(nc.info.Nonce))
sig = base64.StdEncoding.EncodeToString(sigraw)
}

cinfo := connectInfo{o.Verbose, o.Pedantic, user, pass, token,
o.Secure, o.Name, LangString, Version, clientProtoInfo, !o.NoEcho}
cinfo := connectInfo{o.Verbose, o.Pedantic, nkey, sig,
user, pass, token, o.Secure, o.Name, LangString,
Version, clientProtoInfo, !o.NoEcho}

b, err := json.Marshal(cinfo)
if err != nil {
Expand Down Expand Up @@ -2075,6 +2123,7 @@ func (nc *Conn) processInfo(info string) error {
if hasNew && !nc.initc && nc.Opts.DiscoveredServersCB != nil {
nc.ach.push(func() { nc.Opts.DiscoveredServersCB(nc) })
}

return nil
}

Expand Down
55 changes: 55 additions & 0 deletions nats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (

"github.com/nats-io/gnatsd/server"
gnatsd "github.com/nats-io/gnatsd/test"
"github.com/nats-io/nkeys"
)

// Dumb wait program to sync on callbacks, etc... Will timeout
Expand Down Expand Up @@ -1265,3 +1266,57 @@ func TestNoEchoOldServer(t *testing.T) {
t.Fatalf("Expected an error but got none\n")
}
}

const seed = "SUAKYRHVIOREXV7EUZTBHUHL7NUMHPMAS7QMDU3GTIUWEI5LDNOXD43IZY"

func TestNkeyAuth(t *testing.T) {
if server.VERSION[0] == '1' {
t.Skip()
}

kp, _ := nkeys.FromSeed(seed)
pub, _ := kp.PublicKey()

sopts := gnatsd.DefaultTestOptions
sopts.Port = TEST_PORT
sopts.Nkeys = []*server.NkeyUser{&server.NkeyUser{Nkey: pub}}
ts := RunServerWithOptions(sopts)
defer ts.Shutdown()

opts := reconnectOpts
if _, err := opts.Connect(); err == nil {
t.Fatalf("Expected to fail with no nkey auth defined")
}
opts.Nkey = pub
if _, err := opts.Connect(); err != ErrNkeyButNoSigCB {
t.Fatalf("Expected to fail with nkey defined but no signature callback, got %v", err)
}
badSign := func(nonce []byte) []byte {
return []byte("VALID?")
}
opts.SignatureCB = badSign
if _, err := opts.Connect(); err == nil {
t.Fatalf("Expected to fail with nkey and bad signature callback")
}
goodSign := func(nonce []byte) []byte {
sig, err := kp.Sign(nonce)
if err != nil {
t.Fatalf("Failed signing nonce: %v", err)
}
return sig
}
opts.SignatureCB = goodSign
nc, err := opts.Connect()
if err != nil {
t.Fatalf("Expected to succeed but got %v", err)
}

// Now disconnect by killing the server and restarting.
ts.Shutdown()
ts = RunServerWithOptions(sopts)
defer ts.Shutdown()

if err := nc.FlushTimeout(5 * time.Second); err != nil {
t.Fatalf("Error on Flush: %v", err)
}
}

0 comments on commit 733a0cc

Please sign in to comment.