Skip to content

Add ipfs cli 'rotate' command to rotate identity private keys #7515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/ipfs/ipfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var commandsClientCmd = commands.CommandsCmd(Root)
var localCommands = map[string]*cmds.Command{
"daemon": daemonCmd,
"init": initCmd,
"rotate": rotateCmd,
"commands": commandsClientCmd,
}

Expand Down
115 changes: 115 additions & 0 deletions cmd/ipfs/rotate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package main

import (
"fmt"
"io"
"os"

cmds "github.com/ipfs/go-ipfs-cmds"
config "github.com/ipfs/go-ipfs-config"
oldcmds "github.com/ipfs/go-ipfs/commands"
fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
"github.com/ipfs/interface-go-ipfs-core/options"
)

const (
oldKeyOptionName = "oldkey"
)

var rotateCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Rotates the ipfs identity.",
ShortDescription: `
Generates a new ipfs identity and saves it to the ipfs config file.
The daemon must not be running when calling this command.

ipfs uses a repository in the local file system. By default, the repo is
located at ~/.ipfs. To change the repo location, set the $IPFS_PATH
environment variable:

export IPFS_PATH=/path/to/ipfsrepo
`,
},
Arguments: []cmds.Argument{},
Options: []cmds.Option{
cmds.StringOption(oldKeyOptionName, "o", "Keystore name for the old/rotated-out key."),
cmds.StringOption(algorithmOptionName, "a", "Cryptographic algorithm to use for key generation.").WithDefault(algorithmDefault),
cmds.IntOption(bitsOptionName, "b", "Number of bits to use in the generated RSA private key."),
},
PreRun: func(req *cmds.Request, env cmds.Environment) error {
cctx := env.(*oldcmds.Context)
daemonLocked, err := fsrepo.LockedByOtherProcess(cctx.ConfigRoot)
if err != nil {
return err
}

log.Info("checking if daemon is running...")
if daemonLocked {
log.Debug("ipfs daemon is running")
e := "ipfs daemon is running. please stop it to run this command"
return cmds.ClientError(e)
}

return nil
},
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
cctx := env.(*oldcmds.Context)
nBitsForKeypair, nBitsGiven := req.Options[bitsOptionName].(int)
algorithm, _ := req.Options[algorithmOptionName].(string)
oldKey, ok := req.Options[oldKeyOptionName].(string)
if !ok {
return fmt.Errorf("keystore name for backing up old key must be provided")
}
return doRotate(os.Stdout, cctx.ConfigRoot, oldKey, algorithm, nBitsForKeypair, nBitsGiven)
},
}

func doRotate(out io.Writer, repoRoot string, oldKey string, algorithm string, nBitsForKeypair int, nBitsGiven bool) error {
// Open repo
repo, err := fsrepo.Open(repoRoot)
if err != nil {
return fmt.Errorf("opening repo (%v)", err)
}
defer repo.Close()

// Read config file from repo
cfg, err := repo.Config()
if err != nil {
return fmt.Errorf("reading config from repo (%v)", err)
}

// Generate new identity
var identity config.Identity
if nBitsGiven {
identity, err = config.CreateIdentity(out, []options.KeyGenerateOption{
options.Key.Size(nBitsForKeypair),
options.Key.Type(algorithm),
})
} else {
identity, err = config.CreateIdentity(out, []options.KeyGenerateOption{
options.Key.Type(algorithm),
})
}
if err != nil {
return fmt.Errorf("creating identity (%v)", err)
}

// Save old identity to keystore
oldPrivKey, err := cfg.Identity.DecodePrivateKey("")
if err != nil {
return fmt.Errorf("decoding old private key (%v)", err)
}
keystore := repo.Keystore()
if err := keystore.Put(oldKey, oldPrivKey); err != nil {
return fmt.Errorf("saving old key in keystore (%v)", err)
}

// Update identity
cfg.Identity = identity

// Write config file to repo
if err = repo.SetConfig(cfg); err != nil {
return fmt.Errorf("saving new key to config (%v)", err)
}
return nil
}
2 changes: 1 addition & 1 deletion test/sharness/lib/test-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ test_init_ipfs() {

test_expect_success "ipfs init succeeds" '
export IPFS_PATH="$(pwd)/.ipfs" &&
ipfs init --profile=test -b=2048 > /dev/null
ipfs init --profile=test > /dev/null
'

test_expect_success "prepare config -- mounting" '
Expand Down
92 changes: 92 additions & 0 deletions test/sharness/t0027-rotate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash

test_description="Test rotate command"

. lib/test-lib.sh

test_rotate() {
FROM_ALG=$1
TO_ALG=$2
Comment on lines +8 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, although I think we can simplify it so that we:

  1. first test basic rotation on default, RSA, Ed25519 using the test_rotate function which indicates that rotation basically works
  2. test rotating Ed25519 -> RSA -> Ed25519 which indicates that we can rotate to/from each key type. We can even use test_expect_success for these steps to make them easier to debug/notice.


test_expect_success "ipfs init (from $FROM_ALG, to $TO_ALG)" '
export IPFS_PATH="$(pwd)/.ipfs" &&
case $FROM_ALG in
rsa)
ipfs init --profile=test -a=rsa > /dev/null
;;
ed25519)
ipfs init --profile=test -a=ed25519 > /dev/null
;;
*)
ipfs init --profile=test > /dev/null
;;
esac
'

test_expect_success "Save first ID and key" '
ipfs id -f="<id>" > first_id &&
ipfs id -f="<pubkey>" > first_key
'

test_launch_ipfs_daemon

test_kill_ipfs_daemon

test_expect_success "rotating keys" '
case $TO_ALG in
rsa)
ipfs rotate -a=rsa -b=2048 --oldkey=oldkey
;;
ed25519)
ipfs rotate -a=ed25519 --oldkey=oldkey
;;
*)
ipfs rotate --oldkey=oldkey
;;
esac
'

test_expect_success "Compare second ID and key to first" '
ipfs id -f="<id>" > second_id &&
ipfs id -f="<pubkey>" > second_key &&
! test_cmp first_id second_id &&
! test_cmp first_key second_key
'

test_expect_success "checking ID" '
ipfs config Identity.PeerID > expected-id &&
ipfs id -f "<id>\n" > actual-id &&
ipfs key list -l | grep self | cut -d " " -f1 > keystore-id &&
ipfs key list -l | grep oldkey | cut -d " " -f1 | tr -d "\n" > old-keystore-id &&
test_cmp expected-id actual-id &&
test_cmp expected-id keystore-id &&
test_cmp old-keystore-id first_id
'

test_launch_ipfs_daemon

test_expect_success "publish name with new and old keys" '
echo "hello world" > msg &&
ipfs add msg | cut -d " " -f2 | tr -d "\n" > msg_hash &&
ipfs name publish --offline --allow-offline --key=self $(cat msg_hash) &&
ipfs name publish --offline --allow-offline --key=oldkey $(cat msg_hash)
'

test_kill_ipfs_daemon

test_expect_success "clean up ipfs dir" '
rm -rf "$IPFS_PATH"
'

}
test_rotate 'rsa' ''
test_rotate 'ed25519' ''
test_rotate '' ''
test_rotate 'rsa' 'rsa'
test_rotate 'ed25519' 'rsa'
test_rotate '' 'rsa'
test_rotate 'rsa' 'ed25519'
test_rotate 'ed25519' 'ed25519'
test_rotate '' 'ed25519'

test_done