Repository: nats-io/nsc
Pull request #325 (2020-11-25 19:58:25)

[added] check to protect against jwtv2 usage and provide instructions
Signed-off-by: Matthias Hanel <mh@synadia.com>

added check to protect against importing account/operator/user jwtv2
added check to protect against using a jwtv2 store

aricart: Let's update jwt v1 to have a more descriptive error so we can catch any possible load, and the explicit checks will be taken care by this.

Patch:
diff --git a/cmd/addoperator.go b/cmd/addoperator.go
index ce0cba7..d421eb0 100644
--- a/cmd/addoperator.go
+++ b/cmd/addoperator.go
@@ -155,6 +155,9 @@ func (p *AddOperatorParams) Load(ctx ActionCtx) error {
 		}
 		op, err := jwt.DecodeOperatorClaims(token)
 		if err != nil {
+			if err.Error() == JWTV2DecodeError {
+				return fmt.Errorf("%sfailed to decode: %v", JWTUpgradeBannerJWT(), err)
+			}
 			return fmt.Errorf("error importing operator jwt: %v", err)
 		}
 		p.token = token
@@ -330,6 +333,9 @@ func (p *AddOperatorParams) Run(_ ActionCtx) (store.Status, error) {
 		if err == nil {
 			ocNew, err := jwt.DecodeOperatorClaims(p.token)
 			if err != nil {
+				if err.Error() == JWTV2DecodeError {
+					r.AddError("%sfailed to decode: %v", JWTUpgradeBannerJWT(), err)
+				}
 				return nil, err
 			}
 			if oc.Subject != ocNew.Subject {
diff --git a/cmd/importaccount.go b/cmd/importaccount.go
index beeb1c8..bb7bd71 100644
--- a/cmd/importaccount.go
+++ b/cmd/importaccount.go
@@ -127,7 +127,11 @@ func (p *ImportAccount) Run(ctx ActionCtx) (store.Status, error) {
 	theJWT := p.content
 	claim, err := jwt.DecodeAccountClaims(string(theJWT))
 	if err != nil {
-		r.AddError("failed to decode %#q: %v", p.file, err)
+		if err.Error() == JWTV2DecodeError {
+			r.AddError("%sfailed to decode %#q: %v", JWTUpgradeBannerJWT(), p.file, err)
+		} else {
+			r.AddError("failed to decode %#q: %v", p.file, err)
+		}
 		return r, err
 	}
 	if validateAndReport(claim, p.skip, r) {
diff --git a/cmd/importuser.go b/cmd/importuser.go
index 1473d42..5c9eadf 100644
--- a/cmd/importuser.go
+++ b/cmd/importuser.go
@@ -101,7 +101,11 @@ func (p *ImportUser) Run(ctx ActionCtx) (store.Status, error) {
 	}
 	claim, err := jwt.DecodeUserClaims(theJWT)
 	if err != nil {
-		r.AddError("failed to decode %#q: %v", p.file, err)
+		if err.Error() == JWTV2DecodeError {
+			r.AddError("%sfailed to decode %#q: %v", JWTUpgradeBannerJWT(), p.file, err)
+		} else {
+			r.AddError("failed to decode %#q: %v", p.file, err)
+		}
 		return r, err
 	}
 	if validateAndReport(claim, p.skip, r) {
diff --git a/cmd/root.go b/cmd/root.go
index aa11d13..5520cf7 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -43,6 +43,24 @@ var cfgFile string
 
 var ErrNoOperator = errors.New("set an operator -- 'nsc env -o operatorName'")
 
+const JWTV2DecodeError = `unexpected "ed25519-nkey" algorithm`
+
+func JWTUpgradeBannerJWT() string {
+	return fmt.Sprintf(`This version of nsc (%s) is incompatible with the provided jwt.
+If you wish to proceed, you need to upgrade nsc with: "nsc upgrade".
+Note that you’ll always be able to return to this version by typing "nsc upgrade --version %s".
+In order to use jwt v2 you need to upgrade ALL your nats-server to be at least 2.1.9.
+`, GetRootCmd().Version, GetRootCmd().Version)
+}
+
+func JWTUpgradeBannerStore() string {
+	return fmt.Sprintf(`This version of nsc (%s) is incompatible with the stored operator version jwt.
+If you wish to proceed, you need to upgrade nsc with: "nsc upgrade".
+Note that you’ll always be able to return to this version by typing "nsc upgrade --version %s".
+In order to use jwt v2 you need to upgrade ALL your nats-server to be at least 2.1.9.
+`, "", "")
+}
+
 type InterceptorFn func(ctx ActionCtx, params interface{}) error
 
 func GetStoreForOperator(operator string) (*store.Store, error) {
@@ -112,6 +130,11 @@ var rootCmd = &cobra.Command{
 	Use:   "nsc",
 	Short: "nsc creates NATS operators, accounts, users, and manage their permissions.",
 	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+		if store, _ := GetStore(); store != nil {
+			if _, err := store.ReadOperatorClaim(); err != nil && err.Error() == JWTV2DecodeError {
+				return fmt.Errorf(JWTUpgradeBannerStore())
+			}
+		}
 		if cmd.Name() == "migrate" && cmd.Parent().Name() == "keys" {
 			return nil
 		}
diff --git a/go.mod b/go.mod
index d95759e..e9c54b0 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
 	github.com/mitchellh/mapstructure v1.1.2 // indirect
 	github.com/nats-io/cliprompts/v2 v2.0.0-20191226174129-372d79b36768
 	github.com/nats-io/jwt v1.0.1
+	github.com/nats-io/jwt/v2 v2.0.0-20201015190852-e11ce317263c
 	github.com/nats-io/nats-server/v2 v2.1.8-0.20201020210511-496d18468a7d
 	github.com/nats-io/nats.go v1.10.1-0.20200606002146-fc6fed82929a
 	github.com/nats-io/nkeys v0.2.0


Test patch:
diff --git a/cmd/addoperator_test.go b/cmd/addoperator_test.go
index f5382b1..d5636bb 100644
--- a/cmd/addoperator_test.go
+++ b/cmd/addoperator_test.go
@@ -27,6 +27,7 @@ import (
 	"time"
 
 	"github.com/nats-io/jwt"
+	jwtv2 "github.com/nats-io/jwt/v2"
 	"github.com/nats-io/nsc/cmd/store"
 	"github.com/stretchr/testify/require"
 )
@@ -258,3 +259,21 @@ func Test_AddOperatorNameArg(t *testing.T) {
 	require.NoError(t, err)
 	require.Equal(t, "X", oc.Name)
 }
+
+func TestImportOperatorV2(t *testing.T) {
+	ts := NewEmptyStore(t)
+	defer ts.Done(t)
+
+	_, pub, kp := CreateOperatorKey(t)
+	oc := jwtv2.NewOperatorClaims(pub)
+	oc.Name = "O"
+	token, err := oc.Encode(kp)
+	require.NoError(t, err)
+	tf := filepath.Join(ts.Dir, "O.jwt")
+	err = Write(tf, []byte(token))
+	require.NoError(t, err)
+
+	_, stdErr, err := ExecuteCmd(createAddOperatorCmd(), "--url", tf)
+	require.Error(t, err)
+	require.Contains(t, stdErr, JWTUpgradeBannerJWT())
+}
diff --git a/cmd/common_test.go b/cmd/common_test.go
index 7fc3171..e9e20dd 100644
--- a/cmd/common_test.go
+++ b/cmd/common_test.go
@@ -31,7 +31,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/mitchellh/go-homedir"
-
+	jwtv2 "github.com/nats-io/jwt/v2"
 	"github.com/nats-io/nkeys"
 	"github.com/stretchr/testify/require"
 )
@@ -477,3 +477,19 @@ func Test_NameFlagArgOnlyOnEmpty(t *testing.T) {
 		require.Equal(t, v.out, r, "failed test %d", i)
 	}
 }
+
+func Test_OperatorV2(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	dir := filepath.Join(ts.Dir, "store", "O")
+	tf := filepath.Join(dir, "O.jwt")
+	oc := jwtv2.NewOperatorClaims(ts.GetOperatorPublicKey(t))
+	oc.Name = "O"
+	theJwt, err := oc.Encode(ts.OperatorKey)
+	require.NoError(t, err)
+	err = ioutil.WriteFile(tf, []byte(theJwt), 0700)
+	require.NoError(t, err)
+	_, stdErr, err := ExecuteCmd(rootCmd, "env")
+	require.Error(t, err)
+	require.Contains(t, stdErr, JWTUpgradeBannerStore())
+}
diff --git a/cmd/importaccount_test.go b/cmd/importaccount_test.go
index 08895bb..e0c24c6 100644
--- a/cmd/importaccount_test.go
+++ b/cmd/importaccount_test.go
@@ -21,6 +21,7 @@ import (
 	"testing"
 
 	"github.com/nats-io/jwt"
+	jwtv2 "github.com/nats-io/jwt/v2"
 	"github.com/nats-io/nkeys"
 	"github.com/stretchr/testify/require"
 )
@@ -74,3 +75,19 @@ func Test_ImportAccountOtherOperator(t *testing.T) {
 	_, _, err = ExecuteCmd(createImportAccountCmd(), "--file", file)
 	require.Error(t, err)
 }
+
+func Test_ImportAccountV2(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	aKp, _ := nkeys.CreateAccount()
+	pk, _ := aKp.PublicKey()
+	acp := jwtv2.NewAccountClaims(pk)
+	theJWT, err := acp.Encode(aKp) // this account being self signed does not matter for this test
+	require.NoError(t, err)
+	file := filepath.Join(ts.Dir, "account.jwt")
+	err = ioutil.WriteFile(file, []byte(theJWT), 0666)
+	require.NoError(t, err)
+	_, stdErr, err := ExecuteCmd(createImportAccountCmd(), "--file", file)
+	require.Error(t, err)
+	require.Contains(t, stdErr, JWTUpgradeBannerJWT())
+}
diff --git a/cmd/importuser_test.go b/cmd/importuser_test.go
index 047016d..cab4ceb 100644
--- a/cmd/importuser_test.go
+++ b/cmd/importuser_test.go
@@ -21,6 +21,7 @@ import (
 	"testing"
 
 	"github.com/nats-io/jwt"
+	jwtv2 "github.com/nats-io/jwt/v2"
 	"github.com/nats-io/nkeys"
 	"github.com/stretchr/testify/require"
 )
@@ -120,3 +121,21 @@ func Test_ImportUserOtherAccount(t *testing.T) {
 	_, _, err = ExecuteCmd(createImportUserCmd(), "--file", file)
 	require.Error(t, err)
 }
+
+func Test_ImportUserV2(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "bar")
+	aKp, _ := nkeys.CreateAccount() // this account being different does not matter.
+	uKp, _ := nkeys.CreateUser()
+	pk, _ := uKp.PublicKey()
+	ucp := jwtv2.NewUserClaims(pk)
+	theJWT, err := ucp.Encode(aKp)
+	require.NoError(t, err)
+	file := filepath.Join(ts.Dir, "user.jwt")
+	err = ioutil.WriteFile(file, []byte(theJWT), 0666)
+	require.NoError(t, err)
+	_, stdErr, err := ExecuteCmd(createImportUserCmd(), "--file", file)
+	require.Error(t, err)
+	require.Contains(t, stdErr, JWTUpgradeBannerJWT())
+}