Skip to content

Commit

Permalink
Attestation Support (cosmos#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
ejfitzgerald authored Jun 29, 2020
1 parent e04a80c commit 3a3abcd
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 1 deletion.
80 changes: 80 additions & 0 deletions client/attestation/attestation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package attestation

import (
"bytes"
"encoding/hex"
"github.com/tendermint/tendermint/crypto"
)

type Attestation struct {
PublicKey crypto.PubKey
Signature []byte
}

func NewAttestation(key crypto.PrivKey) (*Attestation, error) {

// create the basic attestation
att := &Attestation{
PublicKey: key.PubKey(),
Signature: []byte{},
}

// sign the attestation
err := att.sign(key)
if err != nil {
return nil, err
}

return att, nil
}

func NewAttestationFromString(encoded string) (*Attestation, error) {

// decode the string
bz, err := hex.DecodeString(encoded)
if err != nil {
return nil, err
}

// unmarshall the attestation
att := &Attestation{}
err = UnmarshalBinaryBare(bz, att)
if err != nil {
return nil, err
}

return att, nil
}

func (at *Attestation) sign(key crypto.PrivKey) error {

// sign the payload
signature, err := key.Sign(at.PublicKey.Address().Bytes())
if err != nil {
return err
}

// update the signature
at.Signature = signature

return nil
}

func (at *Attestation) Verify(address crypto.Address) bool {

// ensure that the address derived from the public key matches the required address
if !bytes.Equal(at.PublicKey.Address().Bytes(), address.Bytes()) {
return false
}

// validate the signature present matches the public key
return at.PublicKey.VerifyBytes(at.PublicKey.Address().Bytes(), at.Signature)
}

func (at *Attestation) Bytes() []byte {
return AttestationCdc.MustMarshalBinaryBare(at)
}

func (at *Attestation) String() string {
return hex.EncodeToString(at.Bytes())
}
71 changes: 71 additions & 0 deletions client/attestation/attestation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package attestation

import (
"bytes"
"crypto/rand"
"github.com/cosmos/cosmos-sdk/crypto/keys"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
"testing"
)

func generatePrivateKey(t *testing.T) crypto.PrivKey {

// generate seed for the private key
bz := make([]byte, 32)
_, err := rand.Read(bz)
require.NoError(t, err)

// create the key
return keys.SecpPrivKeyGen(bz)
}

func TestBasicAttestation(t *testing.T) {
privKey := generatePrivateKey(t)

// create the attestation
att, err := NewAttestation(privKey)
require.NoError(t, err)

// verify that it is correct
require.True(t, att.Verify(privKey.PubKey().Address()))
}

func TestBasicAttestationMarshalling(t *testing.T) {
privKey := generatePrivateKey(t)

// create the attestation
att, err := NewAttestation(privKey)
require.NoError(t, err)

// marshall it to binary
bz, err := MarshalBinaryBare(att)
require.NoError(t, err)

// recover the attestation
recoveredAtt := &Attestation{}
err = UnmarshalBinaryBare(bz, recoveredAtt)
require.NoError(t, err)

require.True(t, att.PublicKey.Equals(recoveredAtt.PublicKey))
require.True(t, bytes.Equal(att.Signature, recoveredAtt.Signature))

// check that it sis correct
require.True(t, recoveredAtt.Verify(att.PublicKey.Address()))
}

func TestAttestationAsString(t *testing.T) {
privKey := generatePrivateKey(t)

// create the attestation
att, err := NewAttestation(privKey)
require.NoError(t, err)

// recover the attestation
recovered, err := NewAttestationFromString(att.String())
require.NoError(t, err)

require.True(t, att.PublicKey.Equals(recovered.PublicKey))
require.True(t, bytes.Equal(att.Signature, recovered.Signature))
require.True(t, recovered.Verify(privKey.PubKey().Address()))
}
24 changes: 24 additions & 0 deletions client/attestation/codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package attestation

import (
"github.com/cosmos/cosmos-sdk/codec"
)

var AttestationCdc *codec.Codec

func init() {
AttestationCdc = codec.New()
codec.RegisterCrypto(AttestationCdc)
AttestationCdc.RegisterConcrete(Attestation{}, "cosmos-sdk/Attestation", nil)
AttestationCdc.Seal()
}

// marshal
func MarshalBinaryBare(o interface{}) ([]byte, error) {
return AttestationCdc.MarshalBinaryBare(o)
}

// unmarshal
func UnmarshalBinaryBare(bz []byte, ptr interface{}) error {
return AttestationCdc.UnmarshalBinaryBare(bz, ptr)
}
95 changes: 95 additions & 0 deletions client/keys/attestation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package keys

import (
"bufio"
"errors"
"fmt"
"github.com/cosmos/cosmos-sdk/client/attestation"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/input"
"github.com/cosmos/cosmos-sdk/crypto/keys"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/libs/bech32"
)

func AttestationCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "attestation",
Short: "Create or verify attestations",
Long: `Create or verify offline proofs to demonstrate ownership of keys`,
}

cmd.AddCommand(
&cobra.Command{
Use: "create [name]",
Short: "Create an attestation",
Long: "Create and attestation for one of the keys present in the store",
Args: cobra.ExactArgs(1),
RunE: runAttestationCreate,
},
&cobra.Command{
Use: "verify [address] [attestation]",
Short: "Verify an attestation",
Long: "Given an attestation and address, verify that one proves ownership of the other",
Args: cobra.ExactArgs(2),
RunE: runAttestationVerify,
},
)

return cmd
}

func runAttestationCreate(cmd *cobra.Command, args []string) error {
buf := bufio.NewReader(cmd.InOrStdin())
kb, err := keys.NewKeyring(sdk.KeyringServiceName(), viper.GetString(flags.FlagKeyringBackend), viper.GetString(flags.FlagHome), buf)
if err != nil {
return err
}

decryptPassword, err := input.GetPassword("Enter passphrase to decrypt your key:", buf)
if err != nil {
return err
}

privKey, err := kb.ExportPrivateKeyObject(args[0], decryptPassword)
if err != nil {
return err
}

att, err := attestation.NewAttestation(privKey)
if err != nil {
return err
}

cmd.Println(att.String())

return nil
}

func runAttestationVerify(cmd *cobra.Command, args []string) error {
_, bz, err := bech32.DecodeAndConvert(args[0])
if err != nil {
return err
}

var address crypto.Address = bz

// create the attestation
att, err := attestation.NewAttestationFromString(args[1])
if err != nil {
return err
}

// verification check
verified := att.Verify(address)
if !verified {
return errors.New("verification failed")
}

fmt.Println("verification successful")

return nil
}
2 changes: 2 additions & 0 deletions client/keys/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ func Commands() *cobra.Command {
UpdateKeyCommand(),
ParseKeyStringCommand(),
MigrateCommand(),
flags.LineBreak,
AttestationCmd(),
)
cmd.PersistentFlags().String(flags.FlagKeyringBackend, flags.DefaultKeyringBackend, "Select keyring's backend (os|file|test)")
viper.BindPFlag(flags.FlagKeyringBackend, cmd.Flags().Lookup(flags.FlagKeyringBackend))
Expand Down
2 changes: 1 addition & 1 deletion client/keys/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func TestCommands(t *testing.T) {
assert.NotNil(t, rootCommands)

// Commands are registered
assert.Equal(t, 11, len(rootCommands.Commands()))
assert.Equal(t, 13, len(rootCommands.Commands()))
}

func TestMain(m *testing.M) {
Expand Down
25 changes: 25 additions & 0 deletions server/tm_cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package server

import (
"fmt"
"github.com/cosmos/cosmos-sdk/client/attestation"

"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand Down Expand Up @@ -148,3 +149,27 @@ func UnsafeResetAllCmd(ctx *Context) *cobra.Command {
},
}
}

func AttestationCmd(ctx *Context) *cobra.Command {
cmd := &cobra.Command{
Use: "attestation",
Short: "Create an offline proof that you are in control of this validator address",
RunE: func(cmd *cobra.Command, args []string) error {

cfg := ctx.Config
UpgradeOldPrivValFile(cfg)
privValidator := pvm.LoadOrGenFilePV(
cfg.PrivValidatorKeyFile(), cfg.PrivValidatorStateFile())

att, err := attestation.NewAttestation(privValidator.Key.PrivKey)
if err != nil {
return err
}

fmt.Println(att.String())
return nil
},
}

return cmd
}
1 change: 1 addition & 0 deletions server/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ func AddCommands(
ShowValidatorCmd(ctx),
ShowAddressCmd(ctx),
VersionCmd(ctx),
AttestationCmd(ctx),
)

rootCmd.AddCommand(
Expand Down

0 comments on commit 3a3abcd

Please sign in to comment.