diff --git a/.travis.yml b/.travis.yml index 7897eb94..f57aebe2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ go: - 1.11 install: - go get github.com/nats-io/go-nats +- go get github.com/nats-io/nkeys - go get github.com/mattn/goveralls - go get github.com/wadey/gocovmerge - go get -u honnef.co/go/tools/cmd/megacheck diff --git a/server/auth.go b/server/auth.go index 6d1d8c4f..7ce6d37a 100644 --- a/server/auth.go +++ b/server/auth.go @@ -15,9 +15,11 @@ package server import ( "crypto/tls" + "encoding/base64" "fmt" "strings" + "github.com/nats-io/nkeys" "golang.org/x/crypto/bcrypt" ) @@ -37,6 +39,12 @@ type ClientAuthentication interface { RegisterUser(*User) } +// Nkey is for multiple nkey based users +type NkeyUser struct { + Nkey string `json:"user"` + Permissions *Permissions `json:"permissions"` +} + // User is for multiple accounts/users. type User struct { Username string `json:"user"` @@ -56,6 +64,18 @@ func (u *User) clone() *User { return clone } +// clone performs a deep copy of the NkeyUser struct, returning a new clone with +// all values copied. +func (n *NkeyUser) clone() *NkeyUser { + if n == nil { + return nil + } + clone := &NkeyUser{} + *clone = *n + clone.Permissions = n.Permissions.clone() + return clone +} + // SubjectPermission is an individual allow and deny struct for publish // and subscribe authorizations. type SubjectPermission struct { @@ -125,10 +145,19 @@ func (s *Server) configureAuthorization() { // This just checks and sets up the user map if we have multiple users. if opts.CustomClientAuthentication != nil { s.info.AuthRequired = true - } else if opts.Users != nil { - s.users = make(map[string]*User) - for _, u := range opts.Users { - s.users[u.Username] = u + } else if opts.Nkeys != nil || opts.Users != nil { + // Support both at the same time. + if opts.Nkeys != nil { + s.nkeys = make(map[string]*NkeyUser) + for _, u := range opts.Nkeys { + s.nkeys[u.Nkey] = u + } + } + if opts.Users != nil { + s.users = make(map[string]*User) + for _, u := range opts.Users { + s.users[u.Username] = u + } } s.info.AuthRequired = true } else if opts.Username != "" || opts.Authorization != "" { @@ -152,31 +181,72 @@ func (s *Server) checkAuthorization(c *client) bool { } } -// hasUsers leyt's us know if we have a users array. -func (s *Server) hasUsers() bool { - s.mu.Lock() - hu := s.users != nil - s.mu.Unlock() - return hu -} - // isClientAuthorized will check the client against the proper authorization method and data. -// This could be token or username/password based. +// This could be nkey, token, or username/password based. func (s *Server) isClientAuthorized(c *client) bool { - // Snapshot server options. - opts := s.getOpts() + // Snapshot server options by hand and only grab what we really need. + s.optsMu.RLock() + customClientAuthentication := s.opts.CustomClientAuthentication + authorization := s.opts.Authorization + username := s.opts.Username + password := s.opts.Password + s.optsMu.RUnlock() - // Check custom auth first, then multiple users, then token, then single user/pass. - if opts.CustomClientAuthentication != nil { - return opts.CustomClientAuthentication.Check(c) - } else if s.hasUsers() { - s.mu.Lock() - user, ok := s.users[c.opts.Username] + // Check custom auth first, then nkeys, then multiple users, then token, then single user/pass. + if customClientAuthentication != nil { + return customClientAuthentication.Check(c) + } + + var nkey *NkeyUser + var user *User + var ok bool + + s.mu.Lock() + authRequired := s.info.AuthRequired + if !authRequired { + // TODO(dlc) - If they send us credentials should we fail? s.mu.Unlock() + return true + } + // Check if we have nkeys or users for client. + hasNkeys := s.nkeys != nil + hasUsers := s.users != nil + if hasNkeys && c.opts.Nkey != "" { + nkey, ok = s.nkeys[c.opts.Nkey] if !ok { + s.mu.Unlock() return false } + } else if hasUsers && c.opts.Username != "" { + user, ok = s.users[c.opts.Username] + if !ok { + s.mu.Unlock() + return false + } + } + s.mu.Unlock() + + // Verify the signature against the nonce. + if nkey != nil { + if c.opts.Sig == "" { + return false + } + sig, err := base64.RawURLEncoding.DecodeString(c.opts.Sig) + if err != nil { + return false + } + pub, err := nkeys.FromPublicKey(c.opts.Nkey) + if err != nil { + return false + } + if err := pub.Verify(c.nonce, sig); err != nil { + return false + } + return true + } + + if user != nil { ok = comparePasswords(user.Password, c.opts.Password) // If we are authorized, register the user which will properly setup any permissions // for pub/sub authorizations. @@ -184,18 +254,18 @@ func (s *Server) isClientAuthorized(c *client) bool { c.RegisterUser(user) } return ok - - } else if opts.Authorization != "" { - return comparePasswords(opts.Authorization, c.opts.Authorization) - - } else if opts.Username != "" { - if opts.Username != c.opts.Username { - return false - } - return comparePasswords(opts.Password, c.opts.Password) } - return true + if authorization != "" { + return comparePasswords(authorization, c.opts.Authorization) + } else if username != "" { + if username != c.opts.Username { + return false + } + return comparePasswords(password, c.opts.Password) + } + + return false } // checkRouterAuth checks optional router authorization which can be nil or username/password. diff --git a/server/client.go b/server/client.go index 7e7ae933..c80b5253 100644 --- a/server/client.go +++ b/server/client.go @@ -137,6 +137,7 @@ type client struct { cid uint64 opts clientOpts start time.Time + nonce []byte nc net.Conn ncs string out outbound @@ -246,6 +247,8 @@ type clientOpts struct { Verbose bool `json:"verbose"` Pedantic bool `json:"pedantic"` TLSRequired bool `json:"tls_required"` + Nkey string `json:"nkey"` + Sig string `json:"sig"` Authorization string `json:"auth_token"` Username string `json:"user"` Password string `json:"pass"` diff --git a/server/nkey.go b/server/nkey.go new file mode 100644 index 00000000..b9741629 --- /dev/null +++ b/server/nkey.go @@ -0,0 +1,39 @@ +// Copyright 2018 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "encoding/base64" +) + +// Raw length of the nonce challenge +const ( + nonceRawLen = 16 + nonceLen = 22 // base64.RawURLEncoding.EncodedLen(nonceRawLen) +) + +// nonceRequired tells us if we should send a nonce. +// Assumes server lock is held +func (s *Server) nonceRequired() bool { + return len(s.opts.Nkeys) > 0 +} + +// Generate a nonce for INFO challenge. +// Assumes server lock is held +func (s *Server) generateNonce(n []byte) { + var raw [nonceRawLen]byte + data := raw[:] + s.prand.Read(data) + base64.RawURLEncoding.Encode(n, data) +} diff --git a/server/nkey_test.go b/server/nkey_test.go new file mode 100644 index 00000000..23353b05 --- /dev/null +++ b/server/nkey_test.go @@ -0,0 +1,266 @@ +// Copyright 2018 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "bufio" + crand "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + mrand "math/rand" + "net" + "strings" + "testing" + "time" + + "github.com/nats-io/nkeys" +) + +// Nonce has to be a string since we used different encoding by default than json.Unmarshal. +type nonceInfo struct { + Id string `json:"server_id"` + CID uint64 `json:"client_id,omitempty"` + Nonce string `json:"nonce,omitempty"` +} + +// This is a seed for a user. We can extract public and private keys from this for testing. +const seed = "SUAOB32VQGYIOM622XYNXN4Q6GQR5I6DFZPPYZMNI5MMNVAQZDAL3OLH554ISOUTJM4EC6NWS5UHMS4CMONVVRW3VXXEQULMR6MDLPOEUVFPU" + +func nkeyBasicSetup() (*Server, *client, *bufio.Reader, string) { + kp, _ := nkeys.FromSeed(seed) + pub, _ := kp.PublicKey() + opts := defaultServerOptions + opts.Nkeys = []*NkeyUser{&NkeyUser{Nkey: pub}} + return rawSetup(opts) +} + +func mixedSetup() (*Server, *client, *bufio.Reader, string) { + kp, _ := nkeys.FromSeed(seed) + pub, _ := kp.PublicKey() + opts := defaultServerOptions + opts.Nkeys = []*NkeyUser{&NkeyUser{Nkey: pub}} + opts.Users = []*User{&User{Username: "derek", Password: "foo"}} + return rawSetup(opts) +} + +func newClientForServer(s *Server) (*client, *bufio.Reader, string) { + cli, srv := net.Pipe() + cr := bufio.NewReaderSize(cli, maxBufSize) + ch := make(chan *client) + createClientAsync(ch, s, srv) + l, _ := cr.ReadString('\n') + // Grab client + c := <-ch + return c, cr, l +} + +func TestServerInfoNonce(t *testing.T) { + _, l := setUpClientWithResponse() + if !strings.HasPrefix(l, "INFO ") { + t.Fatalf("INFO response incorrect: %s\n", l) + } + // Make sure payload is proper json + var info nonceInfo + err := json.Unmarshal([]byte(l[5:]), &info) + if err != nil { + t.Fatalf("Could not parse INFO json: %v\n", err) + } + if info.Nonce != "" { + t.Fatalf("Expected an empty nonce with no nkeys defined") + } + + // Now setup server with auth and nkeys to trigger nonce generation + s, _, _, l := nkeyBasicSetup() + + if !strings.HasPrefix(l, "INFO ") { + t.Fatalf("INFO response incorrect: %s\n", l) + } + // Make sure payload is proper json + err = json.Unmarshal([]byte(l[5:]), &info) + if err != nil { + t.Fatalf("Could not parse INFO json: %v\n", err) + } + if info.Nonce == "" { + t.Fatalf("Expected a non-empty nonce with nkeys defined") + } + + // Make sure new clients get new nonces + oldNonce := info.Nonce + + _, _, l = newClientForServer(s) + + err = json.Unmarshal([]byte(l[5:]), &info) + if err != nil { + t.Fatalf("Could not parse INFO json: %v\n", err) + } + if info.Nonce == "" { + t.Fatalf("Expected a non-empty nonce") + } + if strings.Compare(oldNonce, info.Nonce) == 0 { + t.Fatalf("Expected subsequent nonces to be different\n") + } +} + +func TestNkeyClientConnect(t *testing.T) { + s, c, cr, _ := nkeyBasicSetup() + // Send CONNECT with no signature or nkey, should fail. + connectOp := []byte("CONNECT {\"verbose\":true,\"pedantic\":true}\r\n") + go c.parse(connectOp) + l, _ := cr.ReadString('\n') + if !strings.HasPrefix(l, "-ERR ") { + t.Fatalf("Expected an error") + } + + kp, _ := nkeys.FromSeed(seed) + pubKey, _ := kp.PublicKey() + + // Send nkey but no signature + c, cr, _ = newClientForServer(s) + cs := fmt.Sprintf("CONNECT {\"nkey\":%q, \"verbose\":true,\"pedantic\":true}\r\n", pubKey) + connectOp = []byte(cs) + go c.parse(connectOp) + l, _ = cr.ReadString('\n') + if !strings.HasPrefix(l, "-ERR ") { + t.Fatalf("Expected an error") + } + + // Now improperly sign etc. + c, cr, _ = newClientForServer(s) + cs = fmt.Sprintf("CONNECT {\"nkey\":%q,\"sig\":%q,\"verbose\":true,\"pedantic\":true}\r\n", pubKey, "bad_sig") + go c.parse([]byte(cs)) + l, _ = cr.ReadString('\n') + if !strings.HasPrefix(l, "-ERR ") { + t.Fatalf("Expected an error") + } + + // Now properly sign the nonce + c, cr, l = newClientForServer(s) + // Check for Nonce + var info nonceInfo + err := json.Unmarshal([]byte(l[5:]), &info) + if err != nil { + t.Fatalf("Could not parse INFO json: %v\n", err) + } + if info.Nonce == "" { + t.Fatalf("Expected a non-empty nonce with nkeys defined") + } + sigraw, err := kp.Sign([]byte(info.Nonce)) + if err != nil { + t.Fatalf("Failed signing nonce: %v", err) + } + sig := base64.RawURLEncoding.EncodeToString(sigraw) + + // PING needed to flush the +OK to us. + cs = fmt.Sprintf("CONNECT {\"nkey\":%q,\"sig\":\"%s\",\"verbose\":true,\"pedantic\":true}\r\nPING\r\n", pubKey, sig) + go c.parse([]byte(cs)) + l, _ = cr.ReadString('\n') + if !strings.HasPrefix(l, "+OK") { + t.Fatalf("Expected an OK, got: %v", l) + } +} + +func TestMixedClientConnect(t *testing.T) { + s, c, cr, _ := mixedSetup() + // Normal user/pass + // PING needed to flush the +OK to us. + go c.parse([]byte("CONNECT {\"user\":\"derek\",\"pass\":\"foo\",\"verbose\":true,\"pedantic\":true}\r\nPING\r\n")) + l, _ := cr.ReadString('\n') + if !strings.HasPrefix(l, "+OK") { + t.Fatalf("Expected an OK, got: %v", l) + } + + kp, _ := nkeys.FromSeed(seed) + pubKey, _ := kp.PublicKey() + + c, cr, l = newClientForServer(s) + // Check for Nonce + var info nonceInfo + err := json.Unmarshal([]byte(l[5:]), &info) + if err != nil { + t.Fatalf("Could not parse INFO json: %v\n", err) + } + if info.Nonce == "" { + t.Fatalf("Expected a non-empty nonce with nkeys defined") + } + sigraw, err := kp.Sign([]byte(info.Nonce)) + if err != nil { + t.Fatalf("Failed signing nonce: %v", err) + } + sig := base64.RawURLEncoding.EncodeToString(sigraw) + + // PING needed to flush the +OK to us. + cs := fmt.Sprintf("CONNECT {\"nkey\":%q,\"sig\":\"%s\",\"verbose\":true,\"pedantic\":true}\r\nPING\r\n", pubKey, sig) + go c.parse([]byte(cs)) + l, _ = cr.ReadString('\n') + if !strings.HasPrefix(l, "+OK") { + t.Fatalf("Expected an OK, got: %v", l) + } +} + +func BenchmarkCryptoRandGeneration(b *testing.B) { + data := make([]byte, 16) + for i := 0; i < b.N; i++ { + crand.Read(data) + } +} + +func BenchmarkMathRandGeneration(b *testing.B) { + data := make([]byte, 16) + prng := mrand.New(mrand.NewSource(time.Now().UnixNano())) + for i := 0; i < b.N; i++ { + prng.Read(data) + } +} + +func BenchmarkNonceGeneration(b *testing.B) { + data := make([]byte, nonceRawLen) + b64 := make([]byte, nonceLen) + prand := mrand.New(mrand.NewSource(time.Now().UnixNano())) + for i := 0; i < b.N; i++ { + prand.Read(data) + base64.RawURLEncoding.Encode(b64, data) + } +} + +func BenchmarkPublicVerify(b *testing.B) { + data := make([]byte, nonceRawLen) + nonce := make([]byte, nonceLen) + mrand.Read(data) + base64.RawURLEncoding.Encode(nonce, data) + + user, err := nkeys.CreateUser(nil) + if err != nil { + b.Fatalf("Error creating User Nkey: %v", err) + } + sig, err := user.Sign(nonce) + if err != nil { + b.Fatalf("Error sigining nonce: %v", err) + } + pk, err := user.PublicKey() + if err != nil { + b.Fatalf("Could not extract public key from user: %v", err) + } + pub, err := nkeys.FromPublicKey(pk) + if err != nil { + b.Fatalf("Could not create public key pair from public key string: %v", err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := pub.Verify(nonce, sig); err != nil { + b.Fatalf("Error verifying nonce: %v", err) + } + } +} diff --git a/server/opts.go b/server/opts.go index 7a65032f..f1708cc0 100644 --- a/server/opts.go +++ b/server/opts.go @@ -28,6 +28,7 @@ import ( "time" "github.com/nats-io/gnatsd/conf" + "github.com/nats-io/nkeys" ) // ClusterOpts are options for clusters. @@ -59,6 +60,7 @@ type Options struct { Logtime bool `json:"-"` MaxConn int `json:"max_connections"` MaxSubs int `json:"max_subscriptions,omitempty"` + Nkeys []*NkeyUser `json:"-"` Users []*User `json:"-"` Username string `json:"-"` Password string `json:"-"` @@ -110,6 +112,13 @@ func (o *Options) Clone() *Options { clone.Users[i] = user.clone() } } + if o.Nkeys != nil { + clone.Nkeys = make([]*NkeyUser, len(o.Nkeys)) + for i, nkey := range o.Nkeys { + clone.Nkeys[i] = nkey.clone() + } + } + if o.Routes != nil { clone.Routes = make([]*url.URL, len(o.Routes)) for i, route := range o.Routes { @@ -133,7 +142,8 @@ type authorization struct { user string pass string token string - // Multiple Users + // Multiple Nkeys/Users + nkeys []*NkeyUser users []*User timeout float64 defaultPermissions *Permissions @@ -255,6 +265,14 @@ func (o *Options) ProcessConfigFile(configFile string) error { } o.Users = auth.users } + // Check for nkeys + if auth.nkeys != nil { + if o.Users != nil { + return fmt.Errorf("Can not have users when nkeys are also defined.") + } + o.Nkeys = auth.nkeys + } + case "http": hp, err := parseListen(v) if err != nil { @@ -460,7 +478,12 @@ func parseAuthorization(am map[string]interface{}) (*authorization, error) { if err != nil { return nil, err } - auth.users = users + switch users.(type) { + case []*User: + auth.users = users.([]*User) + case []*NkeyUser: + auth.nkeys = users.([]*NkeyUser) + } case "default_permission", "default_permissions", "permissions": pm, ok := mv.(map[string]interface{}) if !ok { @@ -487,22 +510,28 @@ func parseAuthorization(am map[string]interface{}) (*authorization, error) { } // Helper function to parse multiple users array with optional permissions. -func parseUsers(mv interface{}) ([]*User, error) { +func parseUsers(mv interface{}) (interface{}, error) { // Make sure we have an array uv, ok := mv.([]interface{}) if !ok { return nil, fmt.Errorf("Expected users field to be an array, got %v", mv) } - users := []*User{} + var users []*User + var keys []*NkeyUser for _, u := range uv { // Check its a map/struct um, ok := u.(map[string]interface{}) if !ok { return nil, fmt.Errorf("Expected user entry to be a map/struct, got %v", u) } + var perms *Permissions + var err error user := &User{} + nkey := &NkeyUser{} for k, v := range um { switch strings.ToLower(k) { + case "nkey": + nkey.Nkey = v.(string) case "user", "username": user.Username = v.(string) case "pass", "password": @@ -512,18 +541,41 @@ func parseUsers(mv interface{}) ([]*User, error) { if !ok { return nil, fmt.Errorf("Expected user permissions to be a map/struct, got %+v", v) } - permissions, err := parseUserPermissions(pm) + perms, err = parseUserPermissions(pm) if err != nil { return nil, err } - user.Permissions = permissions } } - // Check to make sure we have at least username and password - if user.Username == "" || user.Password == "" { - return nil, fmt.Errorf("User entry requires a user and a password") + // Place perms if we have them. + if perms != nil { + // nkey takes precedent. + if nkey.Nkey != "" { + nkey.Permissions = perms + } else { + user.Permissions = perms + } } - users = append(users, user) + + // Check to make sure we have at least username and password if defined. + if nkey.Nkey == "" && (user.Username == "" || user.Password == "") { + return nil, fmt.Errorf("User entry requires a user and a password") + } else if nkey.Nkey != "" { + // Make sure the nkey is legit. + if !nkeys.IsValidPublicUserKey(nkey.Nkey) { + return nil, fmt.Errorf("Not a valid public nkey for a user") + } + // If we have user or password defined here that is an error. + if user.Username != "" || user.Password != "" { + return nil, fmt.Errorf("Nkey users do not take usernames or passwords") + } + keys = append(keys, nkey) + } else { + users = append(users, user) + } + } + if len(keys) > 0 { + return keys, nil } return users, nil } diff --git a/server/opts_test.go b/server/opts_test.go index a67302bf..93c58fde 100644 --- a/server/opts_test.go +++ b/server/opts_test.go @@ -17,6 +17,7 @@ import ( "bytes" "crypto/tls" "flag" + "fmt" "io/ioutil" "net/url" "os" @@ -761,6 +762,111 @@ func TestNewStyleAuthorizationConfig(t *testing.T) { } } +// Test new nkey users +func TestNkeyUsersConfig(t *testing.T) { + confFileName := "nkeys.conf" + defer os.Remove(confFileName) + content := ` + authorization { + users = [ + {nkey: "UDKTV7HZVYJFJN64LLMYQBUR6MTNNYCDC3LAZH4VHURW3GZLL3FULBXV"} + {nkey: "UA3C5TBZYK5GJQJRWPMU6NFY5JNAEVQB2V2TUZFZDHFJFUYVKTTUOFKZ"} + ] + }` + if err := ioutil.WriteFile(confFileName, []byte(content), 0666); err != nil { + t.Fatalf("Error writing config file: %v", err) + } + opts, err := ProcessConfigFile(confFileName) + if err != nil { + t.Fatalf("Received an error reading config file: %v", err) + } + lu := len(opts.Nkeys) + if lu != 2 { + t.Fatalf("Expected 2 nkey users, got %d", lu) + } +} + +func TestNkeyUsersWithPermsConfig(t *testing.T) { + confFileName := "nkeys.conf" + defer os.Remove(confFileName) + content := ` + authorization { + users = [ + {nkey: "UDKTV7HZVYJFJN64LLMYQBUR6MTNNYCDC3LAZH4VHURW3GZLL3FULBXV", + permissions = { + publish = "$SYSTEM.>" + subscribe = { deny = ["foo", "bar", "baz"] } + } + } + ] + }` + if err := ioutil.WriteFile(confFileName, []byte(content), 0666); err != nil { + t.Fatalf("Error writing config file: %v", err) + } + opts, err := ProcessConfigFile(confFileName) + if err != nil { + t.Fatalf("Received an error reading config file: %v", err) + } + lu := len(opts.Nkeys) + if lu != 1 { + t.Fatalf("Expected 1 nkey user, got %d", lu) + } + nk := opts.Nkeys[0] + if nk.Permissions == nil { + fmt.Printf("nk is %+v\n", nk) + t.Fatal("Expected to have permissions") + } + if nk.Permissions.Publish == nil { + t.Fatal("Expected to have publish permissions") + } + if nk.Permissions.Publish.Allow[0] != "$SYSTEM.>" { + t.Fatalf("Expected publish to allow \"$SYSTEM.>\", but got %v\n", nk.Permissions.Publish.Allow[0]) + } + if nk.Permissions.Subscribe == nil { + t.Fatal("Expected to have subscribe permissions") + } + if nk.Permissions.Subscribe.Allow != nil { + t.Fatal("Expected to have no subscribe allow permissions") + } + deny := nk.Permissions.Subscribe.Deny + if deny == nil || len(deny) != 3 || + deny[0] != "foo" || deny[1] != "bar" || deny[2] != "baz" { + t.Fatalf("Expected to have subscribe deny permissions, got %v", deny) + } +} + +func TestBadNkeyConfig(t *testing.T) { + confFileName := "nkeys_bad.conf" + defer os.Remove(confFileName) + content := ` + authorization { + users = [ {nkey: "Ufoo"}] + }` + if err := ioutil.WriteFile(confFileName, []byte(content), 0666); err != nil { + t.Fatalf("Error writing config file: %v", err) + } + if _, err := ProcessConfigFile(confFileName); err == nil { + t.Fatalf("Expected an error from nkey entry with password") + } +} + +func TestNkeyWithPassConfig(t *testing.T) { + confFileName := "nkeys_pass.conf" + defer os.Remove(confFileName) + content := ` + authorization { + users = [ + {nkey: "UDKTV7HZVYJFJN64LLMYQBUR6MTNNYCDC3LAZH4VHURW3GZLL3FULBXV", pass: "foo"} + ] + }` + if err := ioutil.WriteFile(confFileName, []byte(content), 0666); err != nil { + t.Fatalf("Error writing config file: %v", err) + } + if _, err := ProcessConfigFile(confFileName); err == nil { + t.Fatalf("Expected an error from bad nkey entry") + } +} + func TestTokenWithUserPass(t *testing.T) { confFileName := "test.conf" defer os.Remove(confFileName) diff --git a/server/server.go b/server/server.go index 2a40e64f..0440c03f 100644 --- a/server/server.go +++ b/server/server.go @@ -20,6 +20,7 @@ import ( "flag" "fmt" "io/ioutil" + "math/rand" "net" "net/http" "os" @@ -40,24 +41,25 @@ import ( // Info is the information sent to clients to help them understand information // about this server. type Info struct { - ID string `json:"server_id"` - Version string `json:"version"` - Proto int `json:"proto"` - GitCommit string `json:"git_commit,omitempty"` - GoVersion string `json:"go"` - Host string `json:"host"` - Port int `json:"port"` - AuthRequired bool `json:"auth_required,omitempty"` - TLSRequired bool `json:"tls_required,omitempty"` - TLSVerify bool `json:"tls_verify,omitempty"` - MaxPayload int `json:"max_payload"` - IP string `json:"ip,omitempty"` - CID uint64 `json:"client_id,omitempty"` + ID string `json:"server_id"` + Version string `json:"version"` + Proto int `json:"proto"` + GitCommit string `json:"git_commit,omitempty"` + GoVersion string `json:"go"` + Host string `json:"host"` + Port int `json:"port"` + AuthRequired bool `json:"auth_required,omitempty"` + TLSRequired bool `json:"tls_required,omitempty"` + TLSVerify bool `json:"tls_verify,omitempty"` + MaxPayload int `json:"max_payload"` + IP string `json:"ip,omitempty"` + CID uint64 `json:"client_id,omitempty"` + Nonce string `json:"nonce,omitempty"` + ClientConnectURLs []string `json:"connect_urls,omitempty"` // Contains URLs a client can connect to. // Route Specific - ClientConnectURLs []string `json:"connect_urls,omitempty"` // Contains URLs a client can connect to. - Import *SubjectPermission `json:"import,omitempty"` - Export *SubjectPermission `json:"export,omitempty"` + Import *SubjectPermission `json:"import,omitempty"` + Export *SubjectPermission `json:"export,omitempty"` } // Server is our main struct. @@ -65,6 +67,7 @@ type Server struct { gcid uint64 stats mu sync.Mutex + prand *rand.Rand info Info sl *Sublist configFile string @@ -77,6 +80,7 @@ type Server struct { routes map[uint64]*client remotes map[string]*client users map[string]*User + nkeys map[string]*NkeyUser totalClients uint64 closed *closedRingBuffer done chan bool @@ -165,6 +169,7 @@ func New(opts *Options) *Server { s := &Server{ configFile: opts.ConfigFile, info: info, + prand: rand.New(rand.NewSource(time.Now().UnixNano())), sl: NewSublist(), opts: opts, done: make(chan bool, 1), @@ -749,6 +754,13 @@ func (s *Server) copyInfo() Info { info.ClientConnectURLs = make([]string, len(s.info.ClientConnectURLs)) copy(info.ClientConnectURLs, s.info.ClientConnectURLs) } + if s.nonceRequired() { + // Nonce handling + var raw [nonceLen]byte + nonce := raw[:] + s.generateNonce(nonce) + info.Nonce = string(nonce) + } return info } @@ -765,6 +777,7 @@ func (s *Server) createClient(conn net.Conn) *client { // Grab JSON info string s.mu.Lock() info := s.copyInfo() + c.nonce = []byte(info.Nonce) s.totalClients++ s.mu.Unlock()