Repository: nats-io/nsc
Pull request #528 (2022-12-07 16:03:54)

support for xkey in the authcallout configuration
- [FEAT] added `--curve`, `--rm-curve` key options to `authcallout` command
- [FEAT] updated describer to display curve key
- [FEAT] updated key list to show curve keys associated with an account


Patch:
diff --git a/cmd/describer.go b/cmd/describer.go
index 54f6db2..1fa8a50 100644
--- a/cmd/describer.go
+++ b/cmd/describer.go
@@ -68,6 +68,9 @@ func (a *AccountDescriber) Describe() string {
 		if len(a.Authorization.AllowedAccounts) > 0 {
 			AddListValues(table, "Allowed Accounts", a.Authorization.AllowedAccounts)
 		}
+		if a.Authorization.XKey != "" {
+			table.AddRow("Encrypt For", a.Authorization.XKey)
+		}
 		table.AddSeparator()
 	}
 
diff --git a/cmd/editauthorization.go b/cmd/editauthorization.go
index 083901d..f0c0800 100644
--- a/cmd/editauthorization.go
+++ b/cmd/editauthorization.go
@@ -33,6 +33,8 @@ type EditAccountCalloutParams struct {
 	AllowedAccounts   []string
 	RmAuthUsers       []string
 	RmAllowedAccounts []string
+	XKey              SigningKeysParams
+	RmXKey            bool
 	claim             *jwt.AccountClaims
 }
 
@@ -54,8 +56,8 @@ func createEditAuthorizationCallout() *cobra.Command {
 
 	cmd.Flags().StringSliceVarP(&params.RmAuthUsers, "rm-auth-user", "", nil, "removes a user public key that bypasses the authorization callout and is used by the authorization service itself")
 	cmd.Flags().StringSliceVarP(&params.RmAllowedAccounts, "rm-allowed-account", "", nil, "removes an account public key that the authorization service can bind authorized users to")
-
-	//cmd.Flags().StringVarP(&params.AccountContextParams.Name, "name", "n", "", "account to edit")
+	cmd.Flags().BoolVarP(&params.RmXKey, "rm-curve", "", false, "remove curve encryption target")
+	params.XKey.BindFlags("curve", "x", nkeys.PrefixByteCurve, cmd)
 	params.AccountContextParams.BindFlags(cmd)
 
 	return cmd
@@ -74,10 +76,16 @@ func (p *EditAccountCalloutParams) SetDefaults(ctx ActionCtx) error {
 	p.SignerParams.SetDefaults(nkeys.PrefixByteOperator, true, ctx)
 
 	if ctx.NothingToDo("auth-user", "rm-auth-user", "allowed-account",
-		"rm-allowed-account", "disable") {
+		"rm-allowed-account", "disable", "curve", "rm-curve") {
 		return errors.New("please specify some options")
 	}
 
+	// fail if are given disable and other flags
+	if p.disable && ctx.AnySet("auth-user", "rm-auth-user", "allowed-account",
+		"rm-allowed-account", "curve", "rm-curve") {
+		return errors.New("--disable is exclusive of all other options")
+	}
+
 	return nil
 }
 
@@ -124,10 +132,15 @@ func (p *EditAccountCalloutParams) Validate(ctx ActionCtx) error {
 	}
 
 	if p.disable {
-		// don't look for anything
 		return nil
 	}
 
+	if ctx.AnySet("curve") {
+		if err := p.XKey.Valid(); err != nil {
+			return err
+		}
+	}
+
 	for idx, k := range p.AuthUsers {
 		p.AuthUsers[idx], err = toPublicKey(k, nkeys.PrefixByteUser)
 		if err != nil {
@@ -176,6 +189,8 @@ func (p *EditAccountCalloutParams) Run(ctx ActionCtx) (store.Status, error) {
 	var userRmReport []string
 	var accountReport []string
 	var accountRmReport []string
+	var curveReport []string
+
 	if !p.disable {
 		userReport = report(p.claim.Account.Authorization.AuthUsers,
 			p.AuthUsers,
@@ -200,6 +215,29 @@ func (p *EditAccountCalloutParams) Run(ctx ActionCtx) (store.Status, error) {
 			"deleted account %q",
 			"skipping account %q - as it's not currently set")
 		p.claim.Account.Authorization.AllowedAccounts.Remove(p.RmAllowedAccounts...)
+
+		if p.RmXKey {
+			old := p.claim.Account.Authorization.XKey
+			if old != "" {
+				p.claim.Account.Authorization.XKey = ""
+				curveReport = append(curveReport, fmt.Sprintf("removed %s curve key", old))
+			} else {
+				curveReport = append(curveReport, "remove curve key didn't do anything as the curve key was not set")
+			}
+		}
+
+		if ctx.AnySet("curve") {
+			ckeys, err := p.XKey.PublicKeys()
+			if err != nil {
+				return nil, err
+			}
+			if len(ckeys) > 1 {
+				return nil, errors.New("only one curve key can be specified")
+			}
+			p.claim.Authorization.XKey = ckeys[0]
+			curveReport = append(curveReport, fmt.Sprintf("set curve key %s", ckeys[0]))
+		}
+
 	} else {
 		p.claim.Account.Authorization = jwt.ExternalAuthorization{}
 	}
@@ -233,6 +271,11 @@ func (p *EditAccountCalloutParams) Run(ctx ActionCtx) (store.Status, error) {
 		for _, v := range accountRmReport {
 			r.AddOK(v)
 		}
+
+		for _, v := range curveReport {
+			r.AddOK(v)
+		}
+
 		if p.disable {
 			r.AddOK("removed external authorization configuration")
 		}
diff --git a/cmd/exportkeys.go b/cmd/exportkeys.go
index 7ceb01b..98daae6 100644
--- a/cmd/exportkeys.go
+++ b/cmd/exportkeys.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2019 The NATS Authors
+ * Copyright 2018-2022 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
@@ -67,8 +67,10 @@ nsc export keys --account <name> (changes the account context to the specified a
 	cmd.Flags().BoolVarP(&params.Operator, "operator", "o", false, "export operator keys")
 	cmd.Flags().BoolVarP(&params.Accounts, "accounts", "a", false, "export account keys")
 	cmd.Flags().BoolVarP(&params.Users, "users", "u", false, "export user keys")
+	cmd.Flags().BoolVarP(&params.Curves, "curves", "", false, "export curve keys")
 	cmd.Flags().StringVarP(&params.Account, "account", "", "", "change account context to the named account")
 	cmd.Flags().StringVarP(&params.User, "user", "", "", "export specified user key")
+	cmd.Flags().StringVarP(&params.Curve, "curve", "", "", "export specified curve key")
 	cmd.Flags().BoolVarP(&params.All, "all", "A", false, "export operator, accounts and users keys")
 	cmd.Flags().StringVarP(&params.Filter, "filter", "f", "", "export keys containing string")
 	cmd.Flags().BoolVarP(&params.Unreferenced, "not-referenced", "", false, "export keys that are not referenced in the current operator context")
diff --git a/cmd/generatenkey.go b/cmd/generatenkey.go
index 5377360..ae753c7 100644
--- a/cmd/generatenkey.go
+++ b/cmd/generatenkey.go
@@ -28,6 +28,7 @@ func createGenerateNKeyCmd() *cobra.Command {
 	params.operator.prefix = nkeys.PrefixByteOperator
 	params.account.prefix = nkeys.PrefixByteAccount
 	params.user.prefix = nkeys.PrefixByteUser
+	params.curve.prefix = nkeys.PrefixByteCurve
 
 	cmd := &cobra.Command{
 		Use:   "nkey",
@@ -39,6 +40,7 @@ func createGenerateNKeyCmd() *cobra.Command {
 	cmd.Flags().BoolVarP(&params.operator.generate, "operator", "o", false, "operator")
 	cmd.Flags().BoolVarP(&params.account.generate, "account", "a", false, "account")
 	cmd.Flags().BoolVarP(&params.user.generate, "user", "u", false, "user")
+	cmd.Flags().BoolVarP(&params.curve.generate, "curve", "x", false, "curve")
 	cmd.Flags().BoolVarP(&params.store, "store", "S", false, "store in the keystore")
 
 	return cmd
@@ -52,6 +54,7 @@ type GenerateNKeysParam struct {
 	operator KP
 	account  KP
 	user     KP
+	curve    KP
 	store    bool
 }
 
@@ -70,6 +73,8 @@ func (e *KP) kind() string {
 		return "account"
 	case nkeys.PrefixByteUser:
 		return "user"
+	case nkeys.PrefixByteCurve:
+		return "curve"
 	}
 	return ""
 }
@@ -110,8 +115,8 @@ func (e *KP) String(pubOnly bool) string {
 }
 
 func (p *GenerateNKeysParam) SetDefaults(ctx ActionCtx) error {
-	if !ctx.AnySet("operator", "account", "user") {
-		return fmt.Errorf("set --operator, --account, or --user")
+	if !ctx.AnySet("operator", "account", "user", "curve") {
+		return fmt.Errorf("set --operator, --account, --user, or --curve")
 	}
 	return nil
 }
@@ -135,7 +140,7 @@ func (p *GenerateNKeysParam) Validate(ctx ActionCtx) error {
 func (p *GenerateNKeysParam) Run(ctx ActionCtx) (store.Status, error) {
 	var err error
 	var jobs []*KP
-	jobs = append(jobs, &p.operator, &p.account, &p.user)
+	jobs = append(jobs, &p.operator, &p.account, &p.user, &p.curve)
 	for _, j := range jobs {
 		if j.generate {
 			if err := j.Generate(); err != nil {
diff --git a/cmd/keycollector.go b/cmd/keycollector.go
index aad4406..05e08f8 100644
--- a/cmd/keycollector.go
+++ b/cmd/keycollector.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2019 The NATS Authors
+ * Copyright 2018-2022 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
@@ -16,6 +16,7 @@
 package cmd
 
 import (
+	"fmt"
 	"sort"
 	"strings"
 
@@ -27,17 +28,19 @@ type KeyCollectorParams struct {
 	Operator     bool
 	Accounts     bool
 	Users        bool
+	Curves       bool
 	All          bool
 	Unreferenced bool
 	Account      string
 	User         string
+	Curve        string
 	Filter       string
 }
 
 func (p *KeyCollectorParams) SetDefaults(ctx ActionCtx) error {
 	conf := GetConfig()
 
-	if ctx.NothingToDo("operator", "accounts", "users", "all") {
+	if ctx.NothingToDo("operator", "accounts", "users", "curve", "all") {
 		// default if no args
 		account := p.Account
 		if account == "" {
@@ -46,12 +49,14 @@ func (p *KeyCollectorParams) SetDefaults(ctx ActionCtx) error {
 		p.Operator = true
 		p.Account = account
 		p.Accounts = true
+		p.Curves = true
 		p.Users = true
 	}
 	if p.All {
 		p.Operator = true
 		p.Accounts = true
 		p.Users = true
+		p.Curves = true
 	}
 	return nil
 }
@@ -114,6 +119,14 @@ func (p *KeyCollectorParams) handleAccount(ctx ActionCtx, parent string, name st
 		ask.Resolve(ks)
 		keys = append(keys, &ask)
 	}
+	if ac.Authorization.XKey != "" {
+		var ask Key
+		ask.Name = fmt.Sprintf(ac.Name)
+		ask.Pub = ac.Authorization.XKey
+		ask.Curve = true
+		ask.Resolve(ks)
+		keys = append(keys, &ask)
+	}
 	return keys, nil
 }
 
@@ -212,6 +225,7 @@ func (p *KeyCollectorParams) Run(ctx ActionCtx) (KeyList, error) {
 		var okeys KeyList
 		var akeys KeyList
 		var ukeys KeyList
+		var ckeys KeyList
 		for _, v := range all {
 			_, ok := m[v]
 			if !ok {
@@ -230,16 +244,20 @@ func (p *KeyCollectorParams) Run(ctx ActionCtx) (KeyList, error) {
 					akeys = append(akeys, &k)
 				case nkeys.PrefixByteUser:
 					ukeys = append(ukeys, &k)
+				case nkeys.PrefixByteCurve:
+					ckeys = append(ckeys, &k)
 				}
 			}
 		}
 		sort.Sort(okeys)
 		sort.Sort(akeys)
 		sort.Sort(ukeys)
+		sort.Sort(ckeys)
 		keys = KeyList{}
 		keys = append(keys, okeys...)
 		keys = append(keys, akeys...)
 		keys = append(keys, ukeys...)
+		keys = append(keys, ckeys...)
 	}
 
 	var keyFilter = strings.ToUpper(p.Filter)
@@ -269,6 +287,7 @@ type Key struct {
 	Parent       string
 	ExpectedKind nkeys.PrefixByte
 	Signing      bool
+	Curve        bool
 	KeyPath      string
 	Invalid      bool
 }
diff --git a/cmd/listkeys.go b/cmd/listkeys.go
index 5cfa648..e3e4b2f 100644
--- a/cmd/listkeys.go
+++ b/cmd/listkeys.go
@@ -65,6 +65,7 @@ nsc list keys --account A (changes the account context to the specified account)
 	cmd.Flags().BoolVarP(&params.Operator, "operator", "o", false, "show operator keys")
 	cmd.Flags().BoolVarP(&params.Accounts, "accounts", "a", false, "show account keys")
 	cmd.Flags().BoolVarP(&params.Users, "users", "u", false, "show user keys")
+	cmd.Flags().BoolVarP(&params.Users, "curve", "x", false, "show curve keys")
 	cmd.Flags().StringVarP(&params.Account, "account", "", "", "show specified account keys")
 	cmd.Flags().StringVarP(&params.User, "user", "", "", "show specified user key")
 	cmd.Flags().BoolVarP(&params.All, "all", "A", false, "show operator, accounts and users")
@@ -147,6 +148,9 @@ func (p *ListKeysParams) Report(ks Keys) string {
 		if k.Signing {
 			sk = "*"
 		}
+		if k.Curve {
+			sk = "T"
+		}
 		stored := ""
 		if k.HasKey() {
 			stored = "*"
@@ -170,6 +174,7 @@ func (p *ListKeysParams) Report(ks Keys) string {
 	if hasUnreferenced {
 		s = fmt.Sprintf("%s[?] unreferenced key - may belong to a different operator context", s)
 	}
+	s = fmt.Sprintf("%s[ T ] auth callout encryption target\n", s)
 	return s
 }
 
@@ -182,6 +187,9 @@ func (p *ListKeysParams) ReportSeeds(ks Keys) string {
 		if k.Signing {
 			sk = "*"
 		}
+		if k.Curve {
+			sk = "T"
+		}
 
 		pad := ""
 		switch k.ExpectedKind {
@@ -208,5 +216,6 @@ func (p *ListKeysParams) ReportSeeds(ks Keys) string {
 	s := table.Render()
 	s = fmt.Sprintf("%s[ ! ] seed is not stored\n", s)
 	s = fmt.Sprintf("%s[ERR] error reading seed\n", s)
+	s = fmt.Sprintf("%s[ T ] auth callout encryption target\n", s)
 	return s
 }
diff --git a/cmd/signingkeyseditor.go b/cmd/signingkeyseditor.go
index c6e299d..9e65338 100644
--- a/cmd/signingkeyseditor.go
+++ b/cmd/signingkeyseditor.go
@@ -1,6 +1,6 @@
 /*
  *
- *  * Copyright 2018-2019 The NATS Authors
+ *  * Copyright 2018-2022 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
@@ -35,7 +35,12 @@ type SigningKeysParams struct {
 func (e *SigningKeysParams) BindFlags(flagName string, shorthand string, kind nkeys.PrefixByte, cmd *cobra.Command) {
 	e.flagName = flagName
 	e.kind = kind
-	cmd.Flags().StringSliceVarP(&e.paths, flagName, shorthand, nil, `signing key or keypath or the value "generate"" to generate a key pair on the fly - comma separated list or option can be specified multiple times`)
+	if kind == nkeys.PrefixByteCurve {
+		e.paths = make([]string, 1)
+		cmd.Flags().StringVarP(&e.paths[0], flagName, shorthand, "", `curve key or keypath or the value "generate" to generate a curve encryption target key on the fly`)
+	} else {
+		cmd.Flags().StringSliceVarP(&e.paths, flagName, shorthand, nil, `signing key or keypath or the value "generate" to generate a key pair on the fly - comma separated list or option can be specified multiple times`)
+	}
 }
 
 func (e *SigningKeysParams) valid(s string) error {
@@ -63,16 +68,25 @@ func (e *SigningKeysParams) resolve(s string) (nkeys.KeyPair, error) {
 	if err != nil {
 		return nil, err
 	}
+
 	if kp == nil {
-		return nil, fmt.Errorf("a signing key is required")
+		return nil, fmt.Errorf("a %s key is required", e.label())
 	}
 	if !store.KeyPairTypeOk(e.kind, kp) {
-		return nil, fmt.Errorf("invalid %s signing key %q", e.kind.String(), s)
+		return nil, fmt.Errorf("invalid %s %s key %q", e.kind.String(), e.label(), s)
 	}
 
 	return kp, nil
 }
 
+func (e *SigningKeysParams) label() string {
+	label := "signing"
+	if e.kind == nkeys.PrefixByteCurve {
+		label = "curve"
+	}
+	return label
+}
+
 func (e *SigningKeysParams) Valid() error {
 	for _, v := range e.paths {
 		if err := e.valid(v); err != nil {
@@ -108,10 +122,11 @@ func (e *SigningKeysParams) Edit() error {
 		e.paths[i] = sv
 	}
 	first := true
+
 	for {
-		m := "add a signing key"
+		m := fmt.Sprintf("add a %s key", e.label())
 		if !first || len(e.paths) > 0 {
-			m = "add another signing key"
+			m = fmt.Sprintf("add another %s key", e.label())
 		}
 		first = false
 
diff --git a/cmd/store/keys.go b/cmd/store/keys.go
index 9fbfefe..413cea2 100644
--- a/cmd/store/keys.go
+++ b/cmd/store/keys.go
@@ -480,6 +480,8 @@ func IsPublicKey(kind nkeys.PrefixByte, key string) bool {
 		f = nkeys.IsValidPublicServerKey
 	case nkeys.PrefixByteUser:
 		f = nkeys.IsValidPublicUserKey
+	case nkeys.PrefixByteCurve:
+		f = nkeys.IsValidPublicCurveKey
 	default:
 		return false
 	}
@@ -502,6 +504,9 @@ func PubKeyType(pk string) (nkeys.PrefixByte, error) {
 	if nkeys.IsValidPublicUserKey(pk) {
 		return nkeys.PrefixByteUser, nil
 	}
+	if nkeys.IsValidPublicCurveKey(pk) {
+		return nkeys.PrefixByteCurve, nil
+	}
 	return 0, fmt.Errorf("unsupported key type")
 }
 
diff --git a/go.mod b/go.mod
index 2498a2a..768afcb 100644
--- a/go.mod
+++ b/go.mod
@@ -10,10 +10,10 @@ require (
 	github.com/nats-io/cliprompts/v2 v2.0.0-20200221130455-2737f3b8cbb9
 	github.com/nats-io/jsm.go v0.0.35
 	github.com/nats-io/jwt v1.2.3-0.20210314221642-a826c77dc9d2
-	github.com/nats-io/jwt/v2 v2.3.1-0.20221202180456-83b58fd54838
+	github.com/nats-io/jwt/v2 v2.3.1-0.20221214233435-ff7baa9e5e73
 	github.com/nats-io/nats-server/v2 v2.9.6
 	github.com/nats-io/nats.go v1.21.0
-	github.com/nats-io/nkeys v0.3.0
+	github.com/nats-io/nkeys v0.3.1-0.20221215194120-47c7408e7546
 	github.com/nats-io/nuid v1.0.1
 	github.com/rhysd/go-github-selfupdate v1.2.3
 	github.com/spf13/cobra v1.6.1
diff --git a/go.sum b/go.sum
index 5711f10..e7f706f 100644
--- a/go.sum
+++ b/go.sum
@@ -79,14 +79,15 @@ github.com/nats-io/jsm.go v0.0.35 h1:l03xuGttRA9b81Q0P/WEGm3e5DYof743ZEI4nQR3PUs
 github.com/nats-io/jsm.go v0.0.35/go.mod h1:AkNKZTxbvdFBOJCdlKuLHsRlOP+AI4hV9REQKmq3sWw=
 github.com/nats-io/jwt v1.2.3-0.20210314221642-a826c77dc9d2 h1:Y5jKuPP58S+Fm96nMqInCvSwUKLAm+u8hD96qpiRSCo=
 github.com/nats-io/jwt v1.2.3-0.20210314221642-a826c77dc9d2/go.mod h1:kIiOxe25rMg5oMi63AjFkNK2F1q7ISvH+aaKzAXprDs=
-github.com/nats-io/jwt/v2 v2.3.1-0.20221202180456-83b58fd54838 h1:qfKUzxi+hEeWiC2hjyth7RJI391XQ5nM+pZSx5Y/QbU=
-github.com/nats-io/jwt/v2 v2.3.1-0.20221202180456-83b58fd54838/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
+github.com/nats-io/jwt/v2 v2.3.1-0.20221214233435-ff7baa9e5e73 h1:YPnTZkw0tAZE2zmbG8LlTS0GGfCSPbAu00Gfi+DVi+0=
+github.com/nats-io/jwt/v2 v2.3.1-0.20221214233435-ff7baa9e5e73/go.mod h1:DYujvzCMZzUuqB3i1Pnpf1YtkuTwhdI84Aah9wRXkK0=
 github.com/nats-io/nats-server/v2 v2.9.6 h1:RTtK+rv/4CcliOuqGsy58g7MuWkBaWmF5TUNwuUo9Uw=
 github.com/nats-io/nats-server/v2 v2.9.6/go.mod h1:AB6hAnGZDlYfqb7CTAm66ZKMZy9DpfierY1/PbpvI2g=
 github.com/nats-io/nats.go v1.21.0 h1:kQiWyQMMMIPjDR7NanrLhTnRUxWgU04yrzmYdq9JxCU=
 github.com/nats-io/nats.go v1.21.0/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA=
-github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
 github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
+github.com/nats-io/nkeys v0.3.1-0.20221215194120-47c7408e7546 h1:7ZylVLLiDSFqxJTSibNsO2RjVSXj3QWnDc+zKara2HE=
+github.com/nats-io/nkeys v0.3.1-0.20221215194120-47c7408e7546/go.mod h1:JOEZlxMfMnmaLwr+mpmP+RGIYSxLNBFsZykCGaI2PvA=
 github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
 github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=


Test patch:
diff --git a/cmd/describeaccount_test.go b/cmd/describeaccount_test.go
index f9a4c20..375fade 100644
--- a/cmd/describeaccount_test.go
+++ b/cmd/describeaccount_test.go
@@ -21,7 +21,6 @@ import (
 	"testing"
 
 	"github.com/nats-io/jwt/v2"
-
 	"github.com/stretchr/testify/require"
 )
 
@@ -226,13 +225,16 @@ func TestDescribeAccount_Callout(t *testing.T) {
 
 	_, uPK, _ := CreateUserKey(t)
 	_, aPK, _ := CreateAccountKey(t)
+	_, xPK, _ := CreateCurveKey(t)
 	_, _, err := ExecuteCmd(createEditAuthorizationCallout(),
 		"--auth-user", uPK,
-		"--allowed-account", aPK)
+		"--allowed-account", aPK,
+		"--curve", xPK)
 	require.NoError(t, err)
 
 	out, _, err := ExecuteCmd(createDescribeAccountCmd())
 	require.NoError(t, err)
 	require.Contains(t, out, fmt.Sprintf(" | %s", uPK))
 	require.Contains(t, out, fmt.Sprintf(" | %s", aPK))
+	require.Contains(t, out, fmt.Sprintf(" | %s", xPK))
 }
diff --git a/cmd/editauthorization_test.go b/cmd/editauthorization_test.go
index c8058cb..2779e34 100644
--- a/cmd/editauthorization_test.go
+++ b/cmd/editauthorization_test.go
@@ -18,6 +18,9 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/nats-io/nkeys"
+	"github.com/stretchr/testify/assert"
+
 	"github.com/stretchr/testify/require"
 )
 
@@ -142,3 +145,50 @@ func Test_EditAuthorizationDeleteUser(t *testing.T) {
 	require.Contains(t, ac.Authorization.AllowedAccounts, aPK)
 	require.NotContains(t, ac.Authorization.AllowedAccounts, a2PK)
 }
+
+func Test_EditAuthorizationCurveKey(t *testing.T) {
+	ts := NewTestStore(t, "test")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+
+	_, uPK, _ := CreateUserKey(t)
+	_, u2PK, _ := CreateUserKey(t)
+	_, aPK, _ := CreateAccountKey(t)
+	_, a2PK, _ := CreateAccountKey(t)
+	_, _, err := ExecuteCmd(createEditAuthorizationCallout(),
+		"--auth-user", fmt.Sprintf("%s,%s", uPK, u2PK),
+		"--allowed-account", fmt.Sprintf("%s,%s", aPK, a2PK),
+		"--curve", "generate")
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Contains(t, ac.Authorization.AuthUsers, uPK)
+	require.Contains(t, ac.Authorization.AuthUsers, u2PK)
+	require.Contains(t, ac.Authorization.AllowedAccounts, aPK)
+	require.Contains(t, ac.Authorization.AllowedAccounts, a2PK)
+	require.NotEmpty(t, ac.Authorization.XKey)
+	require.True(t, nkeys.IsValidPublicCurveKey(ac.Authorization.XKey))
+
+	// find the key in the store
+	kp, err := ts.KeyStore.GetKeyPair(ac.Authorization.XKey)
+	require.NoError(t, err)
+	sx, err := kp.Seed()
+	require.NoError(t, err)
+	assert.Equal(t, "SX", string(sx[0:2]))
+
+	_, _, err = ExecuteCmd(createEditAuthorizationCallout(),
+		"--rm-auth-user", u2PK,
+		"--rm-allowed-account", a2PK,
+		"--rm-curve")
+	require.NoError(t, err)
+
+	ac, err = ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Contains(t, ac.Authorization.AuthUsers, uPK)
+	require.NotContains(t, ac.Authorization.AuthUsers, u2PK)
+	require.Contains(t, ac.Authorization.AllowedAccounts, aPK)
+	require.NotContains(t, ac.Authorization.AllowedAccounts, a2PK)
+	require.Empty(t, ac.Authorization.XKey)
+}
diff --git a/cmd/exportkeys_test.go b/cmd/exportkeys_test.go
index 14bc1ad..2ac6d38 100644
--- a/cmd/exportkeys_test.go
+++ b/cmd/exportkeys_test.go
@@ -108,8 +108,17 @@ func Test_ExportAllContext(t *testing.T) {
 	ts.AddAccount(t, "A")
 	ts.AddUser(t, "A", "U")
 
+	upk := ts.GetUserPublicKey(t, "AA", "UU")
+	_, _, err := ExecuteCmd(createEditAuthorizationCallout(), "--account", "AA", "--auth-user", upk, "--curve", "generate")
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("AA")
+	require.NoError(t, err)
+	xpk := ac.Authorization.XKey
+
 	exportDir := filepath.Join(ts.Dir, "export")
-	_, _, err := ExecuteCmd(createExportKeysCmd(), "--all", "--dir", exportDir)
+
+	_, _, err = ExecuteCmd(createExportKeysCmd(), "--all", "--dir", exportDir)
 	require.NoError(t, err)
 
 	opk := ts.GetOperatorPublicKey(t)
@@ -118,7 +127,6 @@ func Test_ExportAllContext(t *testing.T) {
 	apk := ts.GetAccountPublicKey(t, "A")
 	requireExportedKey(t, exportDir, apk)
 
-	upk := ts.GetUserPublicKey(t, "A", "U")
 	requireExportedKey(t, exportDir, upk)
 
 	aapk := ts.GetAccountPublicKey(t, "AA")
@@ -126,6 +134,8 @@ func Test_ExportAllContext(t *testing.T) {
 
 	uupk := ts.GetUserPublicKey(t, "AA", "UU")
 	requireExportedKey(t, exportDir, uupk)
+
+	requireExportedKey(t, exportDir, xpk)
 }
 
 func Test_ExportAccount(t *testing.T) {
@@ -188,3 +198,41 @@ func Test_ExportNoKeyStore(t *testing.T) {
 	require.Error(t, err)
 	require.Equal(t, err.Error(), fmt.Sprintf("keystore `%s` does not exist", ts.KeysDir))
 }
+
+func Test_ExportXKeyNotReferenced(t *testing.T) {
+	ts := NewEmptyStore(t)
+	defer ts.Done(t)
+
+	exportDir := filepath.Join(ts.Dir, "export")
+
+	_, xPK, kp := CreateCurveKey(t)
+	ts.KeyStore.Store(kp)
+	_, _, err := ExecuteCmd(createExportKeysCmd(), "--dir", exportDir, "--curve", xPK, "--not-referenced")
+	require.NoError(t, err)
+	requireExportedKey(t, exportDir, xPK)
+}
+
+func Test_ExportXKeyInContext(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	_, uPK, _ := CreateUserKey(t)
+	_, _, err := ExecuteCmd(createEditAuthorizationCallout(), "--auth-user", uPK, "--curve", "generate")
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	xPK := ac.Authorization.XKey
+	require.NotEmpty(t, xPK)
+
+	exportDir := filepath.Join(ts.Dir, "export")
+	_, _, err = ExecuteCmd(createExportKeysCmd(), "--dir", exportDir, "--curves")
+	require.NoError(t, err)
+	requireExportedKey(t, exportDir, xPK)
+
+	exportDir = filepath.Join(ts.Dir, "export2")
+	_, _, err = ExecuteCmd(createExportKeysCmd(), "--dir", exportDir, "-A")
+	require.NoError(t, err)
+	requireExportedKey(t, exportDir, xPK)
+}
diff --git a/cmd/listkeys_test.go b/cmd/listkeys_test.go
index 4735282..f661ac5 100644
--- a/cmd/listkeys_test.go
+++ b/cmd/listkeys_test.go
@@ -138,3 +138,37 @@ func Test_ListKeysNoKeyStore(t *testing.T) {
 	require.Equal(t, err.Error(), fmt.Sprintf("keystore `%s` does not exist", ts.KeysDir))
 	store.KeyStorePath = old
 }
+
+func Test_listKeysAuthorizationXKey(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	_, uPK, _ := CreateUserKey(t)
+	_, _, err := ExecuteCmd(createEditAuthorizationCallout(), "--auth-user", uPK, "--curve", "generate")
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+
+	xPK := ac.Authorization.XKey
+	require.NotEmpty(t, xPK)
+
+	_, stderr, err := ExecuteCmd(createListKeysCmd(), "-A")
+	require.NoError(t, err)
+	stderr = StripTableDecorations(stderr)
+	require.Contains(t, stderr, xPK)
+
+	_, _, err = ExecuteCmd(createEditAuthorizationCallout(), "--rm-curve")
+	require.NoError(t, err)
+
+	_, stderr, err = ExecuteCmd(createListKeysCmd(), "-A")
+	require.NoError(t, err)
+	stderr = StripTableDecorations(stderr)
+	require.NotContains(t, stderr, xPK)
+
+	_, stderr, err = ExecuteCmd(createListKeysCmd(), "--not-referenced")
+	require.NoError(t, err)
+	stderr = StripTableDecorations(stderr)
+	require.Contains(t, stderr, xPK)
+}
diff --git a/cmd/util_test.go b/cmd/util_test.go
index 1473cca..8603528 100644
--- a/cmd/util_test.go
+++ b/cmd/util_test.go
@@ -382,6 +382,10 @@ func CreateOperatorKey(t *testing.T) (seed []byte, pub string, kp nkeys.KeyPair)
 	return CreateNkey(t, nkeys.PrefixByteOperator)
 }
 
+func CreateCurveKey(t *testing.T) (seed []byte, pub string, kp nkeys.KeyPair) {
+	return CreateNkey(t, nkeys.PrefixByteCurve)
+}
+
 func CreateNkey(t *testing.T, kind nkeys.PrefixByte) ([]byte, string, nkeys.KeyPair) {
 	kp, err := nkeys.CreatePair(kind)
 	require.NoError(t, err)