Skip to content

Commit

Permalink
Add keys subcommands to sign and verify arbitrary text payloads.
Browse files Browse the repository at this point in the history
Closes: #4581
  • Loading branch information
Alessio Treglia committed Sep 18, 2019
1 parent 9c53712 commit b375e1d
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ and tx hash will be returned for specific Tendermint errors:
* `CodeMempoolIsFull`
* `CodeTxTooLarge`
* [\#3872](https://github.com/cosmos/cosmos-sdk/issues/3872) Implement a RESTful endpoint and cli command to decode transactions.
* (cli)[\#4581](https://github.com/cosmos/cosmos-sdk/issues/4581) Add `keys sign` and `keys verify` commands to respectively sign
and verify arbitrary plain text with private keys.

### Improvements

Expand Down
2 changes: 2 additions & 0 deletions client/keys/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ func Commands() *cobra.Command {
deleteKeyCommand(),
updateKeyCommand(),
parseKeyStringCommand(),
signCommand(),
verifyCommand(),
)
return cmd
}
2 changes: 1 addition & 1 deletion client/keys/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ func TestCommands(t *testing.T) {
assert.NotNil(t, rootCommands)

// Commands are registered
assert.Equal(t, 10, len(rootCommands.Commands()))
assert.Equal(t, 12, len(rootCommands.Commands()))
}
70 changes: 70 additions & 0 deletions client/keys/sign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package keys

import (
"bufio"
"fmt"
"io/ioutil"
"os"

"github.com/spf13/cobra"

"github.com/cosmos/cosmos-sdk/client/input"
)

func signCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "sign <name> <filename>",
Short: "Sign a plain text payload with a private key and print the signed document to STDOUT",
Long: `Sign an arbitrary text file with a private key and produce an amino-encoded JSON output.
The signed JSON document could eventually be verified through the 'keys verify' command and will
have the following structure:
{
"text": original text file contents,
"pub": public key,
"sig": signature
}
`,
Args: cobra.ExactArgs(2),
RunE: runSignCmd,
}
cmd.SetOut(os.Stdout)
return cmd
}

func runSignCmd(cmd *cobra.Command, args []string) error {
name := args[0]
filename := args[1]

kb, err := NewKeyBaseFromHomeFlag()
if err != nil {
return err
}

msg, err := ioutil.ReadFile(filename)
if err != nil {
return err
}

buf := bufio.NewReader(cmd.InOrStdin())
passphrase, err := input.GetPassword(fmt.Sprintf("Password to sign with '%s':", name), buf)
if err != nil {
return err
}

sig, pub, err := kb.Sign(name, passphrase, msg)
if err != nil {
return err
}

out, err := MarshalJSON(signedText{
Text: string(msg),
Pub: pub,
Sig: sig,
})
if err != nil {
return err
}

cmd.Println(string(out))
return nil
}
79 changes: 79 additions & 0 deletions client/keys/sign_verify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package keys

import (
"io/ioutil"
"os"
"testing"

"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/libs/cli"

"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/tests"
)

func Test_runSignCmd(t *testing.T) {
signCmd := signCommand()
// err := runSignCmd(signCmd, []string{"invalid", "invalid"})
// require.Contains(t, err.Error(), "no such file or directory")

// Prepare a plain text doc
tmpfile, err := ioutil.TempFile("", "")
require.NoError(t, err)
ioutil.WriteFile(tmpfile.Name(), []byte(`this is
an example`), 0644)
require.NoError(t, err)
tmpfile.Close()
defer os.Remove(tmpfile.Name())

// Prepare a key base
// Now add a temporary keybase
kbHome, cleanUp := tests.NewTestCaseDir(t)
defer cleanUp()
viper.Set(flags.FlagHome, kbHome)
viper.Set(cli.OutputFlag, OutputFormatText)

// Initialise keybase
kb, err := NewKeyBaseFromHomeFlag()
assert.NoError(t, err)
_, err = kb.CreateAccount("key1", tests.TestMnemonic, "", "test1234", 0, 0)
assert.NoError(t, err)

// Mock standard streams
mockIn, mockOut, _ := tests.ApplyMockIO(signCmd)
mockIn.Reset("test1234\n")
require.NoError(t, runSignCmd(signCmd, []string{"key1", tmpfile.Name()}))

signedDocBytes := mockOut.Bytes()
var signedDoc signedText
require.NoError(t, UnmarshalJSON(signedDocBytes, &signedDoc))

// Prepare a signed doc file
outTmpFile, err := ioutil.TempFile("", "")
require.NoError(t, err)
ioutil.WriteFile(outTmpFile.Name(), signedDocBytes, 0644)
require.NoError(t, err)
outTmpFile.Close()
defer os.Remove(outTmpFile.Name())

// Verify
verifyCommand := verifyCommand()
require.NoError(t, runVerifyCmd(verifyCommand, []string{outTmpFile.Name()}))

// Prepare a file with corrupted signature
signedDoc.Text = "this is an example"
signedDocBytes, err = MarshalJSON(signedDoc)
require.NoError(t, err)

// Verification fails
outTmpFile, err = ioutil.TempFile("", "")
require.NoError(t, err)
ioutil.WriteFile(outTmpFile.Name(), signedDocBytes, 0644)
require.NoError(t, err)
outTmpFile.Close()
defer os.Remove(outTmpFile.Name())

require.Error(t, runVerifyCmd(verifyCommand, []string{outTmpFile.Name()}))
}
10 changes: 10 additions & 0 deletions client/keys/types.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package keys

import (
"github.com/tendermint/tendermint/crypto"
)

// used for outputting keys.Info over REST

// AddNewKey request a new key
Expand Down Expand Up @@ -53,3 +57,9 @@ type DeleteKeyReq struct {

// NewDeleteKeyReq constructs a new DeleteKeyReq structure.
func NewDeleteKeyReq(password string) DeleteKeyReq { return DeleteKeyReq{Password: password} }

type signedText struct {
Text string `json:"text"`
Pub crypto.PubKey `json:"pub"`
Sig []byte `json:"sig"`
}
42 changes: 42 additions & 0 deletions client/keys/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package keys

import (
"errors"
"io/ioutil"

"github.com/spf13/cobra"
)

func verifyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "verify <filename>",
Short: "Verify a plain text's signature",
Long: `Read a document generated with the 'key sign' command and verify the signature.
It exits with 0 if the signature verification succeed; it returns a value different than 0
if the signature verification fails.
`,
Args: cobra.ExactArgs(1),
RunE: runVerifyCmd,
}
return cmd
}

func runVerifyCmd(cmd *cobra.Command, args []string) error {
filename := args[0]

signedDoc, err := ioutil.ReadFile(filename)
if err != nil {
return err
}

var doc signedText
if err := UnmarshalJSON(signedDoc, &doc); err != nil {
return err
}

if doc.Pub.VerifyBytes([]byte(doc.Text), doc.Sig) {
cmd.PrintErrln("signature verified")
return nil
}
return errors.New("bad signature")
}

0 comments on commit b375e1d

Please sign in to comment.