Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nkey support #399

Merged
merged 3 commits into from Oct 29, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 63 additions & 14 deletions nats.go
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
derekcollison marked this conversation as resolved.
Show resolved Hide resolved

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no option function to set those, forcing users to use old methods of connecting from Options. I would recommend adding one function that sets the NKey and SignatureCB at the same time, which could ensure that signature is set or otherwise return error. We would still need to do the check outside, though, in case users connection from Options.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree will add it.

// 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 sigCB == nil {
derekcollison marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -1237,6 +1269,15 @@ func (nc *Conn) processExpectedInfo() error {
return err
}

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

// Check if we have an nkey but no signature callback defined.
derekcollison marked this conversation as resolved.
Show resolved Hide resolved
if nc.Opts.Nkey != "" && nc.Opts.SignatureCB == nil {
return ErrNkeyButNoSigCB
}

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
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)
}
}