Repository: nats-io/nsc
Pull request #338 (2020-12-18 00:23:54)

[chore] moved to new signing keys api


Patch:
diff --git a/cmd/addimport.go b/cmd/addimport.go
index 5704717..a5ea9f4 100644
--- a/cmd/addimport.go
+++ b/cmd/addimport.go
@@ -240,7 +240,7 @@ func (p *AddImportParams) generateToken(ctx ActionCtx, c *AccountExportChoice) e
 	// collect the possible signers
 	var signers []string
 	signers = append(signers, srcAC.Subject)
-	signers = append(signers, srcAC.SigningKeys...)
+	signers = append(signers, srcAC.SigningKeys.Keys()...)
 
 	ap.SignerParams.SetPrompt(fmt.Sprintf("select the signing key for account %q [%s]", srcAC.Name, srcAC.Subject))
 	if err := ap.SelectFromSigners(ctx, signers); err != nil {
diff --git a/cmd/adduser.go b/cmd/adduser.go
index 03dcef5..cedc5d4 100644
--- a/cmd/adduser.go
+++ b/cmd/adduser.go
@@ -181,7 +181,7 @@ func validUserSigners(ctx ActionCtx, accName string) ([]string, error) {
 	if ctx.StoreCtx().KeyStore.HasPrivateKey(ac.Subject) {
 		signers = append(signers, ac.Subject)
 	}
-	for _, signingKey := range ac.SigningKeys {
+	for _, signingKey := range ac.SigningKeys.Keys() {
 		if ctx.StoreCtx().KeyStore.HasPrivateKey(signingKey) {
 			signers = append(signers, signingKey)
 		}
diff --git a/cmd/deleteaccount.go b/cmd/deleteaccount.go
index 82afe48..d591095 100644
--- a/cmd/deleteaccount.go
+++ b/cmd/deleteaccount.go
@@ -225,7 +225,7 @@ func (p *DeleteAccountParams) Run(ctx ActionCtx) (store.Status, error) {
 
 	if p.rmNkeys {
 		// delete the account nkeys
-		for _, sk := range p.ac.SigningKeys {
+		for _, sk := range p.ac.SigningKeys.Keys() {
 			if ctx.StoreCtx().KeyStore.HasPrivateKey(sk) {
 				if err := ctx.StoreCtx().KeyStore.Remove(sk); err != nil {
 					r.AddFromError(err)
diff --git a/cmd/describer.go b/cmd/describer.go
index 0771f55..a06fb87 100644
--- a/cmd/describer.go
+++ b/cmd/describer.go
@@ -59,7 +59,7 @@ func (a *AccountDescriber) Describe() string {
 	}
 
 	if len(a.SigningKeys) > 0 {
-		AddListValues(table, "Signing Keys", a.SigningKeys)
+		AddListValues(table, "Signing Keys", a.SigningKeys.Keys())
 		table.AddSeparator()
 	}
 
diff --git a/cmd/fixenv.go b/cmd/fixenv.go
index 74e1281..ae6ad0b 100644
--- a/cmd/fixenv.go
+++ b/cmd/fixenv.go
@@ -281,7 +281,7 @@ func (p *FixCmd) Regenerate(rr *store.Report) error {
 			p.accounts++
 
 			akeys := []string{ac.Subject}
-			akeys = append(akeys, ac.SigningKeys...)
+			akeys = append(akeys, ac.SigningKeys.Keys()...)
 
 			for _, k := range akeys {
 				kp := p.kp(k)
@@ -481,7 +481,7 @@ func (p *FixCmd) LoadAccounts(ctx ActionCtx) (*store.Report, error) {
 						ar.AddOK("ignoring older config %s", src)
 						return nil
 					}
-					for _, sk := range ac.SigningKeys {
+					for _, sk := range ac.SigningKeys.Keys() {
 						p.KeyToPrincipalKey[sk] = ac.Subject
 					}
 				}
diff --git a/cmd/friendlynamecollector.go b/cmd/friendlynamecollector.go
index b0a1024..91a7a93 100644
--- a/cmd/friendlynamecollector.go
+++ b/cmd/friendlynamecollector.go
@@ -58,7 +58,7 @@ func friendlyNames(operator string) (map[string]string, error) {
 				name = fmt.Sprintf("%s/%s", oc.Name, ac.Name)
 			}
 			m[ac.Subject] = name
-			for _, sk := range ac.SigningKeys {
+			for _, sk := range ac.SigningKeys.Keys() {
 				m[sk] = name
 			}
 		}
diff --git a/cmd/generatediagram.go b/cmd/generatediagram.go
index 837ab85..10c9301 100644
--- a/cmd/generatediagram.go
+++ b/cmd/generatediagram.go
@@ -470,7 +470,7 @@ func objectDiagram(users bool, showKeys bool, detail bool) error {
 			defPermId = addPermissions(ac.Subject, ac.Name, "default", ac.DefaultPermissions)
 			bldrPrntf(`%s *-- %s`, ac.Subject, defPermId)
 		}
-		addSigningKeys(ac.Subject, ac.Name, "account", ac.SigningKeys)
+		addSigningKeys(ac.Subject, ac.Name, "account", ac.SigningKeys.Keys())
 
 		connectSigned(op.ClaimsData, ac.ClaimsData)
 		addNote(ac.Subject, ac.Info)
diff --git a/cmd/generateprofile.go b/cmd/generateprofile.go
index 540ab6b..3b40208 100644
--- a/cmd/generateprofile.go
+++ b/cmd/generateprofile.go
@@ -466,7 +466,7 @@ func (p *ProfileCmdParams) getKeys(claim jwt.Claims) []string {
 		}
 		ac, ok := payload.(*jwt.Account)
 		if ok {
-			keys = append(keys, ac.SigningKeys...)
+			keys = append(keys, ac.SigningKeys.Keys()...)
 		}
 	}
 	return keys
diff --git a/cmd/importuser.go b/cmd/importuser.go
index ee70789..d47baec 100644
--- a/cmd/importuser.go
+++ b/cmd/importuser.go
@@ -142,7 +142,7 @@ func (p *ImportUser) Run(ctx ActionCtx) (store.Status, error) {
 	}
 	sameAccount := false
 	keys := []string{accClaim.Subject}
-	keys = append(keys, ([]string(accClaim.SigningKeys))...)
+	keys = append(keys, accClaim.SigningKeys.Keys()...)
 	for _, key := range keys {
 		if key == claim.Issuer {
 			sameAccount = true
diff --git a/cmd/keycollector.go b/cmd/keycollector.go
index e5d66d9..2216c7c 100644
--- a/cmd/keycollector.go
+++ b/cmd/keycollector.go
@@ -106,7 +106,7 @@ func (p *KeyCollectorParams) handleAccount(ctx ActionCtx, parent string, name st
 	aki.Resolve(ks)
 	keys = append(keys, &aki)
 
-	for _, k := range ac.SigningKeys {
+	for _, k := range ac.SigningKeys.Keys() {
 		var ask Key
 		ask.Name = ac.Name
 		ask.Pub = k
diff --git a/cmd/migrate.go b/cmd/migrate.go
index 6bd7a0f..c8d19b8 100644
--- a/cmd/migrate.go
+++ b/cmd/migrate.go
@@ -190,7 +190,7 @@ func (j *MigrateJob) OK() bool {
 func (j *MigrateJob) getAccountKeys() []string {
 	var keys []string
 	keys = append(keys, j.claim.Subject)
-	keys = append(keys, j.claim.SigningKeys...)
+	keys = append(keys, j.claim.SigningKeys.Keys()...)
 	return keys
 }
 
@@ -290,7 +290,7 @@ func (j *MigrateJob) Run(ctx ActionCtx) {
 	if ctx.StoreCtx().Store.IsManaged() {
 		var keys []string
 		keys = append(keys, ac.Subject)
-		keys = append(keys, ac.SigningKeys...)
+		keys = append(keys, ac.SigningKeys.Keys()...)
 
 		// need to sign it with any key we can get
 		var kp nkeys.KeyPair
diff --git a/cmd/store/store.go b/cmd/store/store.go
index 3d96209..2937433 100644
--- a/cmd/store/store.go
+++ b/cmd/store/store.go
@@ -901,7 +901,7 @@ func (ctx *Context) GetAccountKeys(name string) ([]string, error) {
 		return nil, nil
 	}
 	keys = append(keys, ac.Subject)
-	keys = append(keys, ac.SigningKeys...)
+	keys = append(keys, ac.SigningKeys.Keys()...)
 	return keys, nil
 }
 
diff --git a/go.mod b/go.mod
index 6b0aa60..de0aefc 100644
--- a/go.mod
+++ b/go.mod
@@ -11,8 +11,8 @@ require (
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/nats-io/cliprompts/v2 v2.0.0-20191226174129-372d79b36768
 	github.com/nats-io/jwt v1.2.2
-	github.com/nats-io/jwt/v2 v2.0.0-20201211164018-2e78446f4e6f
-	github.com/nats-io/nats-server/v2 v2.1.8-0.20201216194404-d7741b9aa16a
+	github.com/nats-io/jwt/v2 v2.0.0-20201218001130-12bb2c357386
+	github.com/nats-io/nats-server/v2 v2.1.8-0.20201217205918-f09992a889a0
 	github.com/nats-io/nats.go v1.10.1-0.20201021145452-94be476ad6e0
 	github.com/nats-io/nkeys v0.2.0
 	github.com/nats-io/nuid v1.0.1
diff --git a/go.sum b/go.sum
index 074745a..4657392 100644
--- a/go.sum
+++ b/go.sum
@@ -82,13 +82,14 @@ github.com/nats-io/jwt v1.1.0/go.mod h1:n3cvmLfBfnpV4JJRN7lRYCyZnw48ksGsbThGXEk4
 github.com/nats-io/jwt v1.2.2 h1:w3GMTO969dFg+UOKTmmyuu7IGdusK+7Ytlt//OYH/uU=
 github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q=
 github.com/nats-io/jwt/v2 v2.0.0-20200916203241-1f8ce17dff02/go.mod h1:vs+ZEjP+XKy8szkBmQwCB7RjYdIlMaPsFPs4VdS4bTQ=
-github.com/nats-io/jwt/v2 v2.0.0-20201211164018-2e78446f4e6f h1:+F8Vkc9pDf2x6O+csstCXaubPu2njJkbXeTm5wfDzGI=
-github.com/nats-io/jwt/v2 v2.0.0-20201211164018-2e78446f4e6f/go.mod h1:PuO5FToRL31ecdFqVjc794vK0Bj0CwzveQEDvkb7MoQ=
+github.com/nats-io/jwt/v2 v2.0.0-20201217175639-911e216824d5/go.mod h1:PuO5FToRL31ecdFqVjc794vK0Bj0CwzveQEDvkb7MoQ=
+github.com/nats-io/jwt/v2 v2.0.0-20201218001130-12bb2c357386 h1:DlaYlP8H1SwQm1gcw4co/eCLDPgFKneF4DFo0V8ehmI=
+github.com/nats-io/jwt/v2 v2.0.0-20201218001130-12bb2c357386/go.mod h1:PuO5FToRL31ecdFqVjc794vK0Bj0CwzveQEDvkb7MoQ=
 github.com/nats-io/nats-server/v2 v2.1.8-0.20200524125952-51ebd92a9093/go.mod h1:rQnBf2Rv4P9adtAs/Ti6LfFmVtFG6HLhl/H7cVshcJU=
 github.com/nats-io/nats-server/v2 v2.1.8-0.20200601203034-f8d6dd992b71/go.mod h1:Nan/1L5Sa1JRW+Thm4HNYcIDcVRFc5zK9OpSZeI2kk4=
 github.com/nats-io/nats-server/v2 v2.1.8-0.20200929001935-7f44d075f7ad/go.mod h1:TkHpUIDETmTI7mrHN40D1pzxfzHZuGmtMbtb83TGVQw=
-github.com/nats-io/nats-server/v2 v2.1.8-0.20201216194404-d7741b9aa16a h1:xVy7aD956W69q/RBpSpXJDhO5u7hQS5RyLKtC253mKw=
-github.com/nats-io/nats-server/v2 v2.1.8-0.20201216194404-d7741b9aa16a/go.mod h1:p6YiXCODGJlTl0q8AlXWoyCkIYItOd3ZdQuP2m2KR6w=
+github.com/nats-io/nats-server/v2 v2.1.8-0.20201217205918-f09992a889a0 h1:YWRfwiRtCJfYcboXQotvb7I59x9Nuj6Fu/s5V9WD5YQ=
+github.com/nats-io/nats-server/v2 v2.1.8-0.20201217205918-f09992a889a0/go.mod h1:zBJhrc/khJnwPqEFb7c8XMDHl4uv9gAOsXzygd5k2Pk=
 github.com/nats-io/nats.go v1.10.0/go.mod h1:AjGArbfyR50+afOUotNX2Xs5SYHf+CoOa5HH1eEl2HE=
 github.com/nats-io/nats.go v1.10.1-0.20200531124210-96f2130e4d55/go.mod h1:ARiFsjW9DVxk48WJbO3OSZ2DG8fjkMi7ecLmXoY/n9I=
 github.com/nats-io/nats.go v1.10.1-0.20200606002146-fc6fed82929a/go.mod h1:8eAIv96Mo9QW6Or40jUHejS7e4VwZ3VRYD6Sf0BTDp4=
diff --git a/vendor/github.com/nats-io/jwt/v2/Makefile b/vendor/github.com/nats-io/jwt/v2/Makefile
index 53585ce..108c1a4 100644
--- a/vendor/github.com/nats-io/jwt/v2/Makefile
+++ b/vendor/github.com/nats-io/jwt/v2/Makefile
@@ -15,4 +15,4 @@ test:
 	go test -v -coverprofile=./coverage.out ./...
 
 cover:
-	 go tool cover -html=coverage.out
+	 go tool cover -html=coverage.out
\ No newline at end of file
diff --git a/vendor/github.com/nats-io/jwt/v2/account_claims.go b/vendor/github.com/nats-io/jwt/v2/account_claims.go
index 5db453d..2037c11 100644
--- a/vendor/github.com/nats-io/jwt/v2/account_claims.go
+++ b/vendor/github.com/nats-io/jwt/v2/account_claims.go
@@ -89,7 +89,7 @@ type Account struct {
 	Imports            Imports        `json:"imports,omitempty"`
 	Exports            Exports        `json:"exports,omitempty"`
 	Limits             OperatorLimits `json:"limits,omitempty"`
-	SigningKeys        StringList     `json:"signing_keys,omitempty"`
+	SigningKeys        SigningKeys    `json:"signing_keys,omitempty"`
 	Revocations        RevocationList `json:"revocations,omitempty"`
 	DefaultPermissions Permissions    `json:"default_permissions,omitempty"`
 	Info
@@ -126,12 +126,7 @@ func (a *Account) Validate(acct *AccountClaims, vr *ValidationResults) {
 			}
 		}
 	}
-
-	for _, k := range a.SigningKeys {
-		if !nkeys.IsValidPublicAccountKey(k) {
-			vr.AddError("%s is not an account public key", k)
-		}
-	}
+	a.SigningKeys.Validate(vr)
 	a.Info.Validate(vr)
 }
 
@@ -147,6 +142,7 @@ func NewAccountClaims(subject string) *AccountClaims {
 		return nil
 	}
 	c := &AccountClaims{}
+	c.SigningKeys = make(SigningKeys)
 	// Set to unlimited to start. We do it this way so we get compiler
 	// errors if we add to the OperatorLimits.
 	c.Limits = OperatorLimits{
@@ -221,9 +217,9 @@ func (a *AccountClaims) Claims() *ClaimsData {
 }
 
 // DidSign checks the claims against the account's public key and its signing keys
-func (a *AccountClaims) DidSign(op Claims) bool {
-	if op != nil {
-		issuer := op.Claims().Issuer
+func (a *AccountClaims) DidSign(uc Claims) bool {
+	if uc != nil {
+		issuer := uc.Claims().Issuer
 		if issuer == a.Subject {
 			return true
 		}
diff --git a/vendor/github.com/nats-io/jwt/v2/activation_claims.go b/vendor/github.com/nats-io/jwt/v2/activation_claims.go
index eb0c322..49ba3db 100644
--- a/vendor/github.com/nats-io/jwt/v2/activation_claims.go
+++ b/vendor/github.com/nats-io/jwt/v2/activation_claims.go
@@ -51,12 +51,6 @@ func (a *Activation) Validate(vr *ValidationResults) {
 		vr.AddError("invalid export type: %q", a.ImportType)
 	}
 
-	if a.IsService() {
-		if a.ImportSubject.HasWildCards() {
-			vr.AddError("services cannot have wildcard subject: %q", a.ImportSubject)
-		}
-	}
-
 	a.ImportSubject.Validate(vr)
 }
 
diff --git a/vendor/github.com/nats-io/jwt/v2/claims.go b/vendor/github.com/nats-io/jwt/v2/claims.go
index b6d9111..3411e54 100644
--- a/vendor/github.com/nats-io/jwt/v2/claims.go
+++ b/vendor/github.com/nats-io/jwt/v2/claims.go
@@ -31,16 +31,35 @@ import (
 type ClaimType string
 
 const (
-	//OperatorClaim is the type of an operator JWT
+	// OperatorClaim is the type of an operator JWT
 	OperatorClaim = "operator"
 	// AccountClaim is the type of an Account JWT
 	AccountClaim = "account"
-	//UserClaim is the type of an user JWT
+	// UserClaim is the type of an user JWT
 	UserClaim = "user"
-	//ActivationClaim is the type of an activation JWT
+	// ActivationClaim is the type of an activation JWT
 	ActivationClaim = "activation"
+	// GenericClaim is a type that doesn't match Operator/Account/User/ActionClaim
+	GenericClaim = "generic"
 )
 
+func IsGenericClaimType(s string) bool {
+	switch s {
+	case OperatorClaim:
+		fallthrough
+	case AccountClaim:
+		fallthrough
+	case UserClaim:
+		fallthrough
+	case ActivationClaim:
+		return false
+	case GenericClaim:
+		return true
+	default:
+		return true
+	}
+}
+
 // Claims is a JWT claims
 type Claims interface {
 	Claims() *ClaimsData
diff --git a/vendor/github.com/nats-io/jwt/v2/decoder_account.go b/vendor/github.com/nats-io/jwt/v2/decoder_account.go
index 2471b88..607d984 100644
--- a/vendor/github.com/nats-io/jwt/v2/decoder_account.go
+++ b/vendor/github.com/nats-io/jwt/v2/decoder_account.go
@@ -41,6 +41,7 @@ func loadAccount(data []byte, version int) (*AccountClaims, error) {
 		return v1a.Migrate()
 	case 2:
 		var v2a AccountClaims
+		v2a.SigningKeys = make(SigningKeys)
 		if err := json.Unmarshal(data, &v2a); err != nil {
 			return nil, err
 		}
@@ -73,7 +74,10 @@ func (oa v1AccountClaims) migrateV1() (*AccountClaims, error) {
 	a.Account.Limits.AccountLimits = oa.v1NatsAccount.Limits.AccountLimits
 	a.Account.Limits.NatsLimits = oa.v1NatsAccount.Limits.NatsLimits
 	a.Account.Limits.JetStreamLimits = JetStreamLimits{0, 0, 0, 0}
-	a.Account.SigningKeys = oa.v1NatsAccount.SigningKeys
+	a.Account.SigningKeys = make(SigningKeys)
+	for _, v := range oa.SigningKeys {
+		a.Account.SigningKeys.Add(v)
+	}
 	a.Account.Revocations = oa.v1NatsAccount.Revocations
 	a.Version = 1
 	return &a, nil
diff --git a/vendor/github.com/nats-io/jwt/v2/genericlaims.go b/vendor/github.com/nats-io/jwt/v2/genericlaims.go
index 1caf520..6793c9e 100644
--- a/vendor/github.com/nats-io/jwt/v2/genericlaims.go
+++ b/vendor/github.com/nats-io/jwt/v2/genericlaims.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The NATS Authors
+ * Copyright 2018-2020 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
@@ -78,7 +78,10 @@ func DecodeGeneric(token string) (*GenericClaims, error) {
 			return nil, errors.New("claim failed V1 signature verification")
 		}
 		if tp := gc.GenericFields.Type; tp != "" {
-			gc.GenericClaims.Data["type"] = tp
+			// the conversion needs to be from a string because
+			// on custom types the type is not going to be one of
+			// the constants
+			gc.GenericClaims.Data["type"] = string(tp)
 		}
 		if tp := gc.GenericFields.Tags; len(tp) != 0 {
 			gc.GenericClaims.Data["tags"] = tp
@@ -132,8 +135,12 @@ func (gc *GenericClaims) ClaimType() ClaimType {
 			}
 		}
 	}
+
 	switch ct := v.(type) {
 	case string:
+		if IsGenericClaimType(ct) {
+			return GenericClaim
+		}
 		return ClaimType(ct)
 	case ClaimType:
 		return ct
diff --git a/vendor/github.com/nats-io/jwt/v2/imports.go b/vendor/github.com/nats-io/jwt/v2/imports.go
index 478b6de..4acb0b1 100644
--- a/vendor/github.com/nats-io/jwt/v2/imports.go
+++ b/vendor/github.com/nats-io/jwt/v2/imports.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2019 The NATS Authors
+ * Copyright 2018-2020 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
@@ -68,12 +68,6 @@ func (i *Import) Validate(actPubKey string, vr *ValidationResults) {
 
 	i.Subject.Validate(vr)
 
-	if i.IsService() && i.Subject.HasWildCards() {
-		vr.AddError("services cannot have wildcard subject: %q", i.Subject)
-	}
-	if i.IsStream() && i.To.HasWildCards() {
-		vr.AddError("streams cannot have wildcard to subject: %q", i.Subject)
-	}
 	if i.Share && !i.IsService() {
 		vr.AddError("sharing information (for latency tracking) is only valid for services: %q", i.Subject)
 	}
@@ -118,10 +112,7 @@ func (i *Import) Validate(actPubKey string, vr *ValidationResults) {
 			vr.AddError("activation token doesn't match account it is being included in, %q", i.Subject)
 		}
 		act.validateWithTimeChecks(vr, false)
-	} else {
-		vr.AddWarning("no activation provided for import %s", i.Subject)
 	}
-
 }
 
 // Imports is a list of import structs
diff --git a/vendor/github.com/nats-io/jwt/v2/signingkeys.go b/vendor/github.com/nats-io/jwt/v2/signingkeys.go
new file mode 100644
index 0000000..44637ad
--- /dev/null
+++ b/vendor/github.com/nats-io/jwt/v2/signingkeys.go
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2020 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 jwt
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	"github.com/nats-io/nkeys"
+)
+
+type Scope interface {
+	SigningKey() string
+	ValidateScopedSigner(claim Claims) error
+	Validate(vr *ValidationResults)
+}
+
+type ScopeType int
+
+const (
+	UserScopeType ScopeType = iota + 1
+)
+
+func (t ScopeType) String() string {
+	switch t {
+	case UserScopeType:
+		return "user_scope"
+	}
+	return "unknown"
+}
+
+func (t *ScopeType) MarshalJSON() ([]byte, error) {
+	switch *t {
+	case UserScopeType:
+		return []byte("\"user_scope\""), nil
+	}
+	return nil, fmt.Errorf("unknown scope type %q", t)
+}
+
+func (t *ScopeType) UnmarshalJSON(b []byte) error {
+	var s string
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	switch s {
+	case "user_scope":
+		*t = UserScopeType
+		return nil
+	}
+	return fmt.Errorf("unknown scope type %q", t)
+}
+
+type UserScope struct {
+	Kind     ScopeType            `json:"kind"`
+	Key      string               `json:"key"`
+	Role     string               `json:"role"`
+	Template UserPermissionLimits `json:"template"`
+}
+
+func NewUserScope() *UserScope {
+	var s UserScope
+	s.Kind = UserScopeType
+	s.Template.NatsLimits = NatsLimits{NoLimit, NoLimit, NoLimit}
+	return &s
+}
+
+func (us UserScope) SigningKey() string {
+	return us.Key
+}
+
+func (us UserScope) Validate(vr *ValidationResults) {
+	if !nkeys.IsValidPublicAccountKey(us.Key) {
+		vr.AddError("%s is not an account public key", us.Key)
+	}
+}
+
+func (us UserScope) ValidateScopedSigner(c Claims) error {
+	uc, ok := c.(*UserClaims)
+	if !ok {
+		return fmt.Errorf("not an user claim - scoped signing key requires user claim")
+	}
+	if uc.Claims().Issuer != us.Key {
+		return errors.New("issuer not the scoped signer")
+	}
+	if !uc.HasEmptyPermissions() {
+		return errors.New("scoped users require no permissions or limits set")
+	}
+	return nil
+}
+
+// SigningKeys is a map keyed by a public account key
+type SigningKeys map[string]Scope
+
+func (sk SigningKeys) Validate(vr *ValidationResults) {
+	for k, v := range sk {
+		// regular signing keys won't have a scope
+		if v != nil {
+			sv, ok := v.(Scope)
+			if ok {
+				sv.Validate(vr)
+			}
+		} else {
+			if !nkeys.IsValidPublicAccountKey(k) {
+				vr.AddError("%q is not a valid account signing key", k)
+			}
+		}
+	}
+}
+
+// MarshalJSON serializes the scoped signing keys as an array
+func (sk SigningKeys) MarshalJSON() ([]byte, error) {
+	var a []interface{}
+	for k, v := range sk {
+		if v != nil {
+			a = append(a, v)
+		} else {
+			a = append(a, k)
+		}
+	}
+	return json.Marshal(a)
+}
+
+func (sk SigningKeys) UnmarshalJSON(data []byte) error {
+	// read an array - we can have a string or an map
+	var a []interface{}
+	if err := json.Unmarshal(data, &a); err != nil {
+		return err
+	}
+	for _, i := range a {
+		switch v := i.(type) {
+		case string:
+			sk[v] = nil
+		case map[string]interface{}:
+			d, err := json.Marshal(v)
+			if err != nil {
+				return err
+			}
+			switch v["kind"] {
+			case UserScopeType.String():
+				us := NewUserScope()
+				if err := json.Unmarshal(d, &us); err != nil {
+					return err
+				}
+				sk[us.Key] = us
+			default:
+				return fmt.Errorf("unknown signing key scope %q", v["type"])
+			}
+		}
+	}
+	return nil
+}
+
+func (sk SigningKeys) Keys() []string {
+	var keys []string
+	for k := range sk {
+		keys = append(keys, k)
+	}
+	return keys
+}
+
+// GetScope returns nil if the key is not associated
+func (sk SigningKeys) GetScope(k string) (Scope, bool) {
+	v, ok := sk[k]
+	if !ok {
+		return nil, false
+	}
+	return v, true
+}
+
+func (sk SigningKeys) Contains(k string) bool {
+	_, ok := sk[k]
+	return ok
+}
+
+func (sk SigningKeys) Add(keys ...string) {
+	for _, k := range keys {
+		sk[k] = nil
+	}
+}
+
+func (sk SigningKeys) AddScopedSigner(s Scope) {
+	sk[s.SigningKey()] = s
+}
+
+func (sk SigningKeys) Remove(keys ...string) {
+	for _, k := range keys {
+		delete(sk, k)
+	}
+}
diff --git a/vendor/github.com/nats-io/jwt/v2/types.go b/vendor/github.com/nats-io/jwt/v2/types.go
index 78dc9cf..4a42ad7 100644
--- a/vendor/github.com/nats-io/jwt/v2/types.go
+++ b/vendor/github.com/nats-io/jwt/v2/types.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"net"
 	"net/url"
+	"reflect"
 	"strings"
 	"time"
 )
@@ -97,7 +98,7 @@ func (t *ExportType) UnmarshalJSON(b []byte) error {
 		*t = Service
 		return nil
 	}
-	return fmt.Errorf("unknown export type")
+	return fmt.Errorf("unknown export type %q", j)
 }
 
 // Subject is a string that represents a NATS subject
@@ -189,6 +190,10 @@ type UserLimits struct {
 	Locale string      `json:"times_location,omitempty"`
 }
 
+func (u *UserLimits) Empty() bool {
+	return reflect.DeepEqual(*u, UserLimits{})
+}
+
 func (u *UserLimits) IsUnlimited() bool {
 	return len(u.Src) == 0 && len(u.Times) == 0
 }
@@ -233,6 +238,10 @@ type Permission struct {
 	Deny  StringList `json:"deny,omitempty"`
 }
 
+func (p *Permission) Empty() bool {
+	return len(p.Allow) == 0 && len(p.Deny) == 0
+}
+
 // Validate the allow, deny elements of a permission
 func (p *Permission) Validate(vr *ValidationResults) {
 	for _, subj := range p.Allow {
diff --git a/vendor/github.com/nats-io/jwt/v2/user_claims.go b/vendor/github.com/nats-io/jwt/v2/user_claims.go
index f36c74c..0c8ccbe 100644
--- a/vendor/github.com/nats-io/jwt/v2/user_claims.go
+++ b/vendor/github.com/nats-io/jwt/v2/user_claims.go
@@ -17,6 +17,7 @@ package jwt
 
 import (
 	"errors"
+	"reflect"
 
 	"github.com/nats-io/nkeys"
 )
@@ -28,12 +29,16 @@ const (
 	ConnectionTypeMqtt      = "MQTT"
 )
 
-// User defines the user specific data in a user JWT
-type User struct {
+type UserPermissionLimits struct {
 	Permissions
 	Limits
 	BearerToken            bool       `json:"bearer_token,omitempty"`
 	AllowedConnectionTypes StringList `json:"allowed_connection_types,omitempty"`
+}
+
+// User defines the user specific data in a user JWT
+type User struct {
+	UserPermissionLimits
 	// IssuerAccount stores the public key for the account the issuer represents.
 	// When set, the claim was issued by a signing key.
 	IssuerAccount string `json:"issuer_account,omitempty"`
@@ -67,6 +72,21 @@ func NewUserClaims(subject string) *UserClaims {
 	return c
 }
 
+func (u *UserClaims) SetScoped(t bool) {
+	if t {
+		u.UserPermissionLimits = UserPermissionLimits{}
+	} else {
+		u.Limits = Limits{
+			UserLimits{CIDRList{}, nil, ""},
+			NatsLimits{NoLimit, NoLimit, NoLimit},
+		}
+	}
+}
+
+func (u *UserClaims) HasEmptyPermissions() bool {
+	return reflect.DeepEqual(u.UserPermissionLimits, UserPermissionLimits{})
+}
+
 // Encode tries to turn the user claims into a JWT string
 func (u *UserClaims) Encode(pair nkeys.KeyPair) (string, error) {
 	if !nkeys.IsValidPublicUserKey(u.Subject) {
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/accounts.go b/vendor/github.com/nats-io/nats-server/v2/server/accounts.go
index 2802fb9..23b2c69 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/accounts.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/accounts.go
@@ -2844,11 +2844,10 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
 	// update account signing keys
 	a.signingKeys = nil
 	signersChanged := false
-	if len(ac.SigningKeys) > 0 {
-		// insure copy the new keys and sort
-		a.signingKeys = append(a.signingKeys, ac.SigningKeys...)
-		sort.Strings(a.signingKeys)
+	for k := range ac.SigningKeys {
+		a.signingKeys = append(a.signingKeys, k)
 	}
+	sort.Strings(a.signingKeys)
 	if len(a.signingKeys) != len(old.signingKeys) {
 		signersChanged = true
 	} else {
diff --git a/vendor/modules.txt b/vendor/modules.txt
index e26c677..3b6cb61 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -72,10 +72,10 @@ github.com/nats-io/cliprompts/v2
 # github.com/nats-io/jwt v1.2.2
 ## explicit
 github.com/nats-io/jwt
-# github.com/nats-io/jwt/v2 v2.0.0-20201211164018-2e78446f4e6f
+# github.com/nats-io/jwt/v2 v2.0.0-20201218001130-12bb2c357386
 ## explicit
 github.com/nats-io/jwt/v2
-# github.com/nats-io/nats-server/v2 v2.1.8-0.20201216194404-d7741b9aa16a
+# github.com/nats-io/nats-server/v2 v2.1.8-0.20201217205918-f09992a889a0
 ## explicit
 github.com/nats-io/nats-server/v2/conf
 github.com/nats-io/nats-server/v2/internal/ldap


Test patch:
diff --git a/cmd/friendlynamecollector_test.go b/cmd/friendlynamecollector_test.go
index 75d1807..8544546 100644
--- a/cmd/friendlynamecollector_test.go
+++ b/cmd/friendlynamecollector_test.go
@@ -73,7 +73,7 @@ func Test_FriendlyNameCollector(t *testing.T) {
 	require.Equal(t, "O", m[oc.Subject])
 	require.Equal(t, "O", m[oc.SigningKeys[0]])
 	require.Equal(t, "A", m[aac.Subject])
-	require.Equal(t, "A", m[aac.SigningKeys[0]])
+	require.Equal(t, "A", m[aac.SigningKeys.Keys()[0]])
 	require.Equal(t, "B", m[bac.Subject])
 
 	ts.AddOperator(t, "OO")
@@ -93,6 +93,6 @@ func Test_FriendlyNameCollector(t *testing.T) {
 	require.Equal(t, "O", m[oc.Subject])
 	require.Equal(t, "O", m[oc.SigningKeys[0]])
 	require.Equal(t, "O/A", m[aac.Subject])
-	require.Equal(t, "O/A", m[aac.SigningKeys[0]])
+	require.Equal(t, "O/A", m[aac.SigningKeys.Keys()[0]])
 	require.Equal(t, "O/B", m[bac.Subject])
 }