Repository: nats-io/nsc
Pull request #349 (2021-01-20 18:08:27)

[added] command to re-issue operator identity key
Also resigns affected accounts

Signed-off-by: Matthias Hanel <mh@synadia.com>

```
> nsc -h
nsc creates NATS operators, accounts, users, and manage their permissions.

Usage:
  nsc [command]

Available Commands:
  add         Add assets such as accounts, imports, users
  delete      Delete imports and exports
  describe    Describe assets such as operators, accounts, users, and jwt files
  edit        Edit assets such as accounts, imports, and users
  env         Prints and manage the nsc environment
  export      Export assets such as nkeys
  generate    Generate activations, creds, configs or nkeys
  help        Help about any command
  import      Import assets such as nkeys
  init        Initialize an environment by creating an operator, account and user
  keys        Manage keys for operators, accounts, and users
  list        List assets such as accounts, imports, users
  pull        Pull an operator or account jwt replacing the local jwt with the server's version
  push        Push an account jwt to an Account JWT Server
  reissue     Re-issue objects with a new identity key
  revocations Manage revocation for users and activations from an account
  tool        NATS tools: pub, sub, req, rep, rtt
  update      Update this tool to latest version
  validate    Validate an operator, account(s), and users

Flags:
  -h, --help                 help for nsc
  -i, --interactive          ask questions for various settings
  -K, --private-key string   private key
      --version              version for nsc

Use "nsc [command] --help" for more information about a command.
> nsc reissue -h
Re-issue objects with a new identity key

Usage:
  nsc reissue [command]

Available Commands:
  operator    Re-issues operator with a new identity and re-signs affected accounts

Flags:
  -h, --help   help for reissue

Global Flags:
  -i, --interactive          ask questions for various settings
  -K, --private-key string   private key

Use "nsc reissue [command] --help" for more information about a command.
> re-issue-operator *2 +2 ?1
```


Patch:
diff --git a/cmd/reissueoperator.go b/cmd/reissueoperator.go
new file mode 100644
index 0000000..92d6581
--- /dev/null
+++ b/cmd/reissueoperator.go
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2020-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 cmd
+
+import (
+	"fmt"
+
+	"github.com/nats-io/nkeys"
+	"github.com/nats-io/nsc/cmd/store"
+	"github.com/spf13/cobra"
+)
+
+func createReIssueOperatorCmd() *cobra.Command {
+	var params reIssueOperator
+	cmd := &cobra.Command{
+		Use:          "operator",
+		Short:        "Re-issues the operator with a new identity and re-signs affected accounts",
+		Example:      `nsc reissue operator`,
+		Args:         MaxArgs(0),
+		SilenceUsage: false,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if err := RunMaybeStorelessAction(cmd, args, &params); err != nil {
+				return err
+			}
+			return nil
+		},
+	}
+	cmd.Flags().BoolVarP(&params.turnIntoSigningKey, "convert-to-signing-key", "", false,
+		"turn operator identity key into signing key (avoids account re-signing)")
+	return cmd
+}
+
+// removeCmd represents the resign command
+var reIssue = &cobra.Command{
+	Use:   "reissue",
+	Short: "Re-issue objects with a new identity key",
+}
+
+func init() {
+	reIssue.AddCommand(createReIssueOperatorCmd())
+	GetRootCmd().AddCommand(reIssue)
+}
+
+type reIssueOperator struct {
+	turnIntoSigningKey bool
+}
+
+func (p *reIssueOperator) PreInteractive(ctx ActionCtx) error {
+	return nil
+}
+
+func (p *reIssueOperator) Load(ctx ActionCtx) error {
+	return nil
+}
+
+func (p *reIssueOperator) PostInteractive(ctx ActionCtx) error {
+	return nil
+}
+
+func (p *reIssueOperator) SetDefaults(ctx ActionCtx) error {
+	return nil
+}
+
+func (p *reIssueOperator) Validate(ctx ActionCtx) error {
+	if ctx.StoreCtx().Store.IsManaged() {
+		return fmt.Errorf("resign is only supported in non managed stores")
+	}
+	if _, err := ctx.StoreCtx().Store.ReadOperatorClaim(); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (p *reIssueOperator) Run(ctx ActionCtx) (store.Status, error) {
+	r := store.NewDetailedReport(true)
+	op, err := ctx.StoreCtx().Store.ReadOperatorClaim()
+	if err != nil {
+		r.AddError("failed to obtain operator: %v", err)
+		return r, err
+	}
+	opKp, err := nkeys.CreateOperator()
+	if err != nil {
+		r.AddError("failed to generate new operator identity: %v", err)
+		return r, err
+	}
+	if _, err := ctx.StoreCtx().KeyStore.Store(opKp); err != nil {
+		r.AddError("failed to store new operator identity: %v", err)
+		return r, err
+	}
+	opPub, err := opKp.PublicKey()
+	if err != nil {
+		r.AddError("failed to obtain public key from new identity: %v", err)
+		return r, err
+	}
+	oldPubKey := op.Subject
+	if p.turnIntoSigningKey {
+		op.SigningKeys.Add(oldPubKey)
+	}
+	op.Subject = opPub
+	if opJWT, err := op.Encode(opKp); err != nil {
+		r.AddError("failed to encode new operator jwt: %v", err)
+		return r, err
+	} else if err := ctx.StoreCtx().Store.StoreRaw([]byte(opJWT)); err != nil {
+		r.AddError("failed to store new operator jwt: %v", err)
+		return r, err
+	}
+	r.AddOK("operator %q successfully changed identity to: %s", op.Name, opPub)
+	if p.turnIntoSigningKey {
+		r.AddOK("old operator key %q turned into signing key", opPub)
+		return r, nil
+	}
+	accounts, err := ctx.StoreCtx().Store.ListSubContainers(store.Accounts)
+	if err != nil {
+		r.AddError("failed to obtain accounts: %v", err)
+	}
+	for _, acName := range accounts {
+		claim, err := ctx.StoreCtx().Store.ReadAccountClaim(acName)
+		if err != nil {
+			r.AddError("failed reading account %s: %v", acName, err)
+		}
+		if claim.Issuer != oldPubKey {
+			r.AddOK("account %q already signed properly", acName)
+			continue
+		}
+		if accJWT, err := claim.Encode(opKp); err != nil {
+			r.AddError("account %q error during encoding: %v", acName, err)
+		} else if err := ctx.StoreCtx().Store.StoreRaw([]byte(accJWT)); err != nil {
+			r.AddError("failed to store new account jwt for account %q: %v", acName, err)
+		}
+		r.AddOK("account %q re-signed", acName)
+	}
+	return r, nil
+}


Test patch:
diff --git a/cmd/reissueoperator_test.go b/cmd/reissueoperator_test.go
new file mode 100644
index 0000000..b01f829
--- /dev/null
+++ b/cmd/reissueoperator_test.go
@@ -0,0 +1,61 @@
+/*
+ * 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 cmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func Test_ReIssue(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	op1, err := ts.Store.ReadOperatorClaim()
+	require.NoError(t, err)
+	_, _, err = ExecuteCmd(createReIssueOperatorCmd())
+	require.NoError(t, err)
+	op2, err := ts.Store.ReadOperatorClaim()
+	require.NoError(t, err)
+	require.NotEqual(t, op1.Subject, op2.Subject)
+	require.Len(t, op1.SigningKeys, 0)
+	// add testing account
+	ts.AddAccount(t, "A")
+
+	_, _, err = ExecuteCmd(createReIssueOperatorCmd(), "--convert-to-signing-key")
+	require.NoError(t, err)
+	op3, err := ts.Store.ReadOperatorClaim()
+	require.NoError(t, err)
+	require.NotEqual(t, op2.Subject, op3.Subject)
+	require.Len(t, op3.SigningKeys, 1)
+	require.True(t, op3.SigningKeys.Contains(op2.Subject))
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.True(t, op3.DidSign(ac))
+
+	_, _, err = ExecuteCmd(createReIssueOperatorCmd())
+	require.NoError(t, err)
+	op4, err := ts.Store.ReadOperatorClaim()
+	require.NoError(t, err)
+	require.NotEqual(t, op3.Subject, op4.Subject)
+	require.Len(t, op4.SigningKeys, 1)
+	require.True(t, op4.SigningKeys.Contains(op2.Subject))
+
+	ac, err = ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.True(t, op4.DidSign(ac))
+}