diff --git a/README.md b/README.md index 933a1ef..f664947 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,24 @@ c.Subscribe("help", func(subj, reply string, msg string) { c.Close(); ``` +## Nkey Authentication (server versions >= 2.0.0) +To authenticate with nkeys, the nkey seed should be in a read only file, e.g. seed.txt +```bash +> cat seed.txt +# This is my seed nkey! +SUAGMJH5XLGZKQQWAWKRZJIGMOU4HPFUYLXJMXOO5NLFEO2OOQJ5LPRDPM +``` + +This is a helper function which will load and decode and do the proper signing for the server nonce. +It will clear memory in between invocations. +You can choose to use the low level option and provide the public key and a signature callback on your own. + +```go +opt, err := nats.NkeyOptionFromSeed("seed.txt") +nc, err := nats.Connect(serverUrl, opt) + +``` + ## TLS ```go diff --git a/examples/nats-bench.go b/examples/nats-bench.go index d22e1f6..4b05718 100644 --- a/examples/nats-bench.go +++ b/examples/nats-bench.go @@ -11,6 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +// +build ignore + package main import ( diff --git a/examples/nats-echo.go b/examples/nats-echo.go new file mode 100644 index 0000000..dc5974a --- /dev/null +++ b/examples/nats-echo.go @@ -0,0 +1,89 @@ +// 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. + +// +build ignore + +package main + +import ( + "flag" + "log" + "runtime" + + "github.com/nats-io/go-nats" +) + +// NOTE: Can test with demo servers. +// nats-echo -s demo.nats.io +// nats-echo -s demo.nats.io:4443 (TLS version) + +func usage() { + log.Fatalf("Usage: nats-echo [-s server] [-t] [-nkey seedfile] ") +} + +func printMsg(m *nats.Msg, i int) { + log.Printf("[#%d] Echoing %q", i, m.Data) +} + +func main() { + var urls = flag.String("s", nats.DefaultURL, "The nats server URLs (separated by comma)") + var nkeyFile = flag.String("nkey", "", "Use the nkey seed file for authentication") + var showTime = flag.Bool("t", false, "Display timestamps") + + log.SetFlags(0) + flag.Usage = usage + flag.Parse() + + args := flag.Args() + if len(args) != 1 { + usage() + } + + // general options. + opts := []nats.Option{nats.Name("NATS Echo Service")} + + // Use Nkey authentication. + if *nkeyFile != "" { + opt, err := nats.NkeyOptionFromSeed(*nkeyFile) + if err != nil { + log.Fatal(err) + } + opts = append(opts, opt) + } + + nc, err := nats.Connect(*urls, opts...) + if err != nil { + log.Fatalf("Can't connect: %v\n", err) + } + + subj, i := args[0], 0 + + nc.Subscribe(subj, func(msg *nats.Msg) { + i++ + printMsg(msg, i) + // Just echo back what they sent us. + nc.Publish(msg.Reply, msg.Data) + }) + nc.Flush() + + if err := nc.LastError(); err != nil { + log.Fatal(err) + } + + log.Printf("Echo Service listening on [%s]\n", subj) + if *showTime { + log.SetFlags(log.LstdFlags) + } + + runtime.Goexit() +} diff --git a/examples/nats-pub.go b/examples/nats-pub.go index c129f50..5b191e1 100644 --- a/examples/nats-pub.go +++ b/examples/nats-pub.go @@ -22,24 +22,40 @@ import ( "github.com/nats-io/go-nats" ) -// NOTE: Use tls scheme for TLS, e.g. nats-pub -s tls://demo.nats.io:4443 foo hello +// NOTE: Can test with demo servers. +// nats-pub -s demo.nats.io +// nats-pub -s demo.nats.io:4443 (TLS version) + func usage() { - log.Fatalf("Usage: nats-pub [-s server (%s)] \n", nats.DefaultURL) + log.Fatalf("Usage: nats-pub [-s server] ") } func main() { var urls = flag.String("s", nats.DefaultURL, "The nats server URLs (separated by comma)") + var nkeyFile = flag.String("nkey", "", "Use the nkey seed file for authentication") log.SetFlags(0) flag.Usage = usage flag.Parse() args := flag.Args() - if len(args) < 2 { + if len(args) != 2 { usage() } - nc, err := nats.Connect(*urls) + // general options. + opts := []nats.Option{nats.Name("NATS Sample Publisher")} + + // Use Nkey authentication. + if *nkeyFile != "" { + opt, err := nats.NkeyOptionFromSeed(*nkeyFile) + if err != nil { + log.Fatal(err) + } + opts = append(opts, opt) + } + + nc, err := nats.Connect(*urls, opts...) if err != nil { log.Fatal(err) } diff --git a/examples/nats-qsub.go b/examples/nats-qsub.go index 05e7267..8182975 100644 --- a/examples/nats-qsub.go +++ b/examples/nats-qsub.go @@ -24,9 +24,12 @@ import ( "github.com/nats-io/go-nats" ) -// NOTE: Use tls scheme for TLS, e.g. nats-qsub -s tls://demo.nats.io:4443 foo +// NOTE: Can test with demo servers. +// nats-qsub -s demo.nats.io +// nats-qsub -s demo.nats.io:4443 (TLS version) + func usage() { - log.Fatalf("Usage: nats-qsub [-s server] [-t] \n") + log.Fatalf("Usage: nats-qsub [-s server] [-t] ") } func printMsg(m *nats.Msg, i int) { @@ -35,6 +38,7 @@ func printMsg(m *nats.Msg, i int) { func main() { var urls = flag.String("s", nats.DefaultURL, "The nats server URLs (separated by comma)") + var nkeyFile = flag.String("nkey", "", "Use the nkey seed file for authentication") var showTime = flag.Bool("t", false, "Display timestamps") log.SetFlags(0) @@ -42,11 +46,23 @@ func main() { flag.Parse() args := flag.Args() - if len(args) < 2 { + if len(args) != 2 { usage() } - nc, err := nats.Connect(*urls) + // general options. + opts := []nats.Option{nats.Name("NATS Sample Queue Subscriber")} + + // Use Nkey authentication. + if *nkeyFile != "" { + opt, err := nats.NkeyOptionFromSeed(*nkeyFile) + if err != nil { + log.Fatal(err) + } + opts = append(opts, opt) + } + + nc, err := nats.Connect(*urls, opts...) if err != nil { log.Fatalf("Can't connect: %v\n", err) } diff --git a/examples/nats-req.go b/examples/nats-req.go index b552024..7f7bafb 100644 --- a/examples/nats-req.go +++ b/examples/nats-req.go @@ -23,13 +23,17 @@ import ( "github.com/nats-io/go-nats" ) -// NOTE: Use tls scheme for TLS, e.g. nats-req -s tls://demo.nats.io:4443 foo hello +// NOTE: Can test with demo servers. +// nats-req -s demo.nats.io +// nats-req -s demo.nats.io:4443 (TLS version) + func usage() { - log.Fatalf("Usage: nats-req [-s server (%s)] \n", nats.DefaultURL) + log.Fatalf("Usage: nats-req [-s server] ") } func main() { var urls = flag.String("s", nats.DefaultURL, "The nats server URLs (separated by comma)") + var nkeyFile = flag.String("nkey", "", "Use the nkey seed file for authentication") log.SetFlags(0) flag.Usage = usage @@ -40,14 +44,26 @@ func main() { usage() } - nc, err := nats.Connect(*urls) + // general options. + opts := []nats.Option{nats.Name("NATS Sample Requestor")} + + // Use Nkey authentication. + if *nkeyFile != "" { + opt, err := nats.NkeyOptionFromSeed(*nkeyFile) + if err != nil { + log.Fatal(err) + } + opts = append(opts, opt) + } + + nc, err := nats.Connect(*urls, opts...) if err != nil { log.Fatalf("Can't connect: %v\n", err) } defer nc.Close() subj, payload := args[0], []byte(args[1]) - msg, err := nc.Request(subj, []byte(payload), 500*time.Millisecond) + msg, err := nc.Request(subj, []byte(payload), time.Second) if err != nil { if nc.LastError() != nil { log.Fatalf("Error in Request: %v\n", nc.LastError()) diff --git a/examples/nats-rply.go b/examples/nats-rply.go index 8ea09e2..3a35966 100644 --- a/examples/nats-rply.go +++ b/examples/nats-rply.go @@ -23,9 +23,12 @@ import ( "github.com/nats-io/go-nats" ) -// NOTE: Use tls scheme for TLS, e.g. nats-rply -s tls://demo.nats.io:4443 foo hello +// NOTE: Can test with demo servers. +// nats-rply -s demo.nats.io +// nats-rply -s demo.nats.io:4443 (TLS version) + func usage() { - log.Fatalf("Usage: nats-rply [-s server][-t] \n") + log.Fatalf("Usage: nats-rply [-s server] [-t] ") } func printMsg(m *nats.Msg, i int) { @@ -34,6 +37,7 @@ func printMsg(m *nats.Msg, i int) { func main() { var urls = flag.String("s", nats.DefaultURL, "The nats server URLs (separated by comma)") + var nkeyFile = flag.String("nkey", "", "Use the nkey seed file for authentication") var showTime = flag.Bool("t", false, "Display timestamps") log.SetFlags(0) @@ -45,7 +49,19 @@ func main() { usage() } - nc, err := nats.Connect(*urls) + // general options. + opts := []nats.Option{nats.Name("NATS Sample Responder")} + + // Use Nkey authentication. + if *nkeyFile != "" { + opt, err := nats.NkeyOptionFromSeed(*nkeyFile) + if err != nil { + log.Fatal(err) + } + opts = append(opts, opt) + } + + nc, err := nats.Connect(*urls, opts...) if err != nil { log.Fatalf("Can't connect: %v\n", err) } diff --git a/examples/nats-sub.go b/examples/nats-sub.go index 68c1983..2928226 100644 --- a/examples/nats-sub.go +++ b/examples/nats-sub.go @@ -23,9 +23,12 @@ import ( "github.com/nats-io/go-nats" ) -// NOTE: Use tls scheme for TLS, e.g. nats-sub -s tls://demo.nats.io:4443 foo +// NOTE: Can test with demo servers. +// nats-sub -s demo.nats.io +// nats-sub -s demo.nats.io:4443 (TLS version) + func usage() { - log.Fatalf("Usage: nats-sub [-s server] [-t] \n") + log.Fatalf("Usage: nats-sub [-s server] [-t] ") } func printMsg(m *nats.Msg, i int) { @@ -34,6 +37,7 @@ func printMsg(m *nats.Msg, i int) { func main() { var urls = flag.String("s", nats.DefaultURL, "The nats server URLs (separated by comma)") + var nkeyFile = flag.String("nkey", "", "Use the nkey seed file for authentication") var showTime = flag.Bool("t", false, "Display timestamps") log.SetFlags(0) @@ -41,11 +45,23 @@ func main() { flag.Parse() args := flag.Args() - if len(args) < 1 { + if len(args) != 1 { usage() } - nc, err := nats.Connect(*urls) + // general options. + opts := []nats.Option{nats.Name("NATS Sample Subscriber")} + + // Use Nkey authentication. + if *nkeyFile != "" { + opt, err := nats.NkeyOptionFromSeed(*nkeyFile) + if err != nil { + log.Fatal(err) + } + opts = append(opts, opt) + } + + nc, err := nats.Connect(*urls, opts...) if err != nil { log.Fatalf("Can't connect: %v\n", err) } diff --git a/nats.go b/nats.go index 625bc81..37b6435 100644 --- a/nats.go +++ b/nats.go @@ -36,6 +36,7 @@ import ( "time" "github.com/nats-io/go-nats/util" + "github.com/nats-io/nkeys" "github.com/nats-io/nuid" ) @@ -3544,3 +3545,74 @@ func (nc *Conn) GetClientID() (uint64, error) { } return nc.info.CID, nil } + +// NkeyOptionFromSeed will load an nkey pair from a seed file. +// It will return the NKey Option and will handle +// signing of nonce challenges from the server. It will take +// care to not hold keys in memory and to wipe memory. +func NkeyOptionFromSeed(seedFile string) (Option, error) { + kp, err := nkeyPairFromSeedFile(seedFile) + if err != nil { + return nil, err + } + // Wipe our key on exit. + defer kp.Wipe() + + pub, err := kp.PublicKey() + if err != nil { + return nil, err + } + if !nkeys.IsValidPublicUserKey(pub) { + return nil, fmt.Errorf("nats: Not a valid nkey user seed") + } + sigCB := func(nonce []byte) []byte { + return sigHandler(nonce, seedFile) + } + return Nkey(string(pub), sigCB), nil +} + +func nkeyPairFromSeedFile(seedFile string) (nkeys.KeyPair, error) { + var seed []byte + contents, err := ioutil.ReadFile(seedFile) + if err != nil { + return nil, fmt.Errorf("nats: %v", err) + } + defer wipeSlice(contents) + + lines := bytes.Split(contents, []byte("\n")) + for _, line := range lines { + if bytes.HasPrefix(line, []byte("SU")) { + seed = line + break + } + } + if seed == nil { + return nil, fmt.Errorf("nats: No nkey user seed found in %q", seedFile) + } + kp, err := nkeys.FromSeed(seed) + if err != nil { + return nil, err + } + return kp, nil +} + +// Sign authentication challenges from the server. +// Do not keep private seed in memory. +func sigHandler(nonce []byte, seedFile string) []byte { + kp, err := nkeyPairFromSeedFile(seedFile) + if err != nil { + return nil + } + // Wipe our key on exit. + defer kp.Wipe() + + sig, _ := kp.Sign(nonce) + return sig +} + +// Just wipe slice with 'x', for clearing contents of nkey seed file. +func wipeSlice(buf []byte) { + for i := range buf { + buf[i] = 'x' + } +} diff --git a/nats_test.go b/nats_test.go index e1a1e12..afb9e5d 100644 --- a/nats_test.go +++ b/nats_test.go @@ -1267,19 +1267,18 @@ func TestNoEchoOldServer(t *testing.T) { } } -const seed = "SUAKYRHVIOREXV7EUZTBHUHL7NUMHPMAS7QMDU3GTIUWEI5LDNOXD43IZY" - func TestNkeyAuth(t *testing.T) { if server.VERSION[0] == '1' { t.Skip() } + seed := []byte("SUAKYRHVIOREXV7EUZTBHUHL7NUMHPMAS7QMDU3GTIUWEI5LDNOXD43IZY") kp, _ := nkeys.FromSeed(seed) pub, _ := kp.PublicKey() sopts := gnatsd.DefaultTestOptions sopts.Port = TEST_PORT - sopts.Nkeys = []*server.NkeyUser{&server.NkeyUser{Nkey: pub}} + sopts.Nkeys = []*server.NkeyUser{&server.NkeyUser{Nkey: string(pub)}} ts := RunServerWithOptions(sopts) defer ts.Shutdown() @@ -1287,7 +1286,7 @@ func TestNkeyAuth(t *testing.T) { if _, err := opts.Connect(); err == nil { t.Fatalf("Expected to fail with no nkey auth defined") } - opts.Nkey = pub + opts.Nkey = string(pub) if _, err := opts.Connect(); err != ErrNkeyButNoSigCB { t.Fatalf("Expected to fail with nkey defined but no signature callback, got %v", err) }