Skip to content

Commit

Permalink
Replace subkey with go port (#114)
Browse files Browse the repository at this point in the history
* Move to go.mod

* Replace subkey with go port

* Update README file

* Refactor fixed array type conversion

* Fix linter error

* Expand tests

* Update changes with new version of subkey go port

* Fix linter errors
  • Loading branch information
mace authored Jan 19, 2021
1 parent 3b7101e commit f23fc78
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 122 deletions.
11 changes: 1 addition & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
# Note: We don't use Alpine and its packaged Rust/Cargo because they're too often out of date,
# preventing them from being used to build Substrate/Polkadot.

# First Phase - Load Subkey
FROM parity/subkey:2.0.0 as subkey
RUN subkey --version

## Second Phase - Build context for tests
## First Phase - Build context for tests
FROM parity/substrate:v2.0.0

USER root

COPY --from=subkey /usr/local/bin/subkey /usr/local/bin/subkey

# gcc for cgo
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ \
Expand Down Expand Up @@ -65,9 +59,6 @@ RUN mkdir -p $GOPATH/src/github.com/centrifuge/go-substrate-rpc-client
WORKDIR $GOPATH/src/github.com/centrifuge/go-substrate-rpc-client
COPY . .

# Ensuring Subkey is available
RUN subkey --version

RUN make install

# Reset parent entrypoint
Expand Down
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ This client is modelled after [polkadot-js/api](https://github.com/polkadot-js/a

This package is feature complete, but it is relatively new and might still contain bugs. We advice to use it with caution in production. It comes without any warranties, please refer to LICENCE for details.

## Requirements
Substrate Key Management requires `subkey` to be present in your PATH: https://substrate.dev/docs/en/knowledgebase/integrate/subkey

The `subkey` recommended version: https://github.com/paritytech/substrate/releases/tag/v2.0.0-rc6

## Documentation & Usage Examples

Please refer to https://godoc.org/github.com/centrifuge/go-substrate-rpc-client
Expand Down
9 changes: 4 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ require (
github.com/btcsuite/btcutil v1.0.2
github.com/davecgh/go-spew v1.1.1
github.com/deckarep/golang-set v1.7.1
github.com/ethereum/go-ethereum v1.9.3
github.com/go-stack/stack v1.8.0 // indirect
github.com/ethereum/go-ethereum v1.9.25
github.com/gorilla/websocket v1.4.1
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pierrec/xxHash v0.1.5
github.com/rs/cors v1.6.0
github.com/stretchr/testify v1.3.0
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect
github.com/stretchr/testify v1.6.1
github.com/vedhavyas/go-subkey v1.0.2
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce
)
211 changes: 207 additions & 4 deletions go.sum

Large diffs are not rendered by default.

119 changes: 35 additions & 84 deletions signature/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,16 @@
package signature

import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"strconv"

"github.com/vedhavyas/go-subkey"
"github.com/vedhavyas/go-subkey/sr25519"
"golang.org/x/crypto/blake2b"
)

const subkeyCmd = "subkey"

type KeyringPair struct {
// URI is the derivation path for the private key in subkey
URI string
Expand All @@ -38,52 +36,25 @@ type KeyringPair struct {
PublicKey []byte
}

// InspectKeyInfo type is used as target from `subkey` inspect JSON output
type InspectKeyInfo struct {
AccountID string `json:"accountId"`
PublicKey string `json:"publicKey"`
SecretPhrase string `json:"secretPhrase"`
SecretSeed string `json:"secretSeed"`
SS58Address string `json:"ss58Address"`
}

// KeyringPairFromSecret creates KeyPair based on seed/phrase and network
// Leave network empty for default behavior
func KeyringPairFromSecret(seedOrPhrase, network string) (KeyringPair, error) {
var args []string
if network != "" {
args = []string{"--network", network}
}
args = append([]string{"inspect", "--output-type", "Json", seedOrPhrase}, args...)

// use "subkey" command for creation of public key and address
cmd := exec.Command(subkeyCmd, args...)

// execute the command, get the output
out, err := cmd.Output()
func KeyringPairFromSecret(seedOrPhrase string, network uint8) (KeyringPair, error) {
scheme := sr25519.Scheme{}
kyr, err := subkey.DeriveKeyPair(scheme, seedOrPhrase)
if err != nil {
return KeyringPair{}, fmt.Errorf("failed to generate keyring pair from secret: %v", err.Error())
return KeyringPair{}, err
}

if string(out) == "Invalid phrase/URI given" {
return KeyringPair{}, fmt.Errorf("failed to generate keyring pair from secret: invalid phrase/URI given")
}

var keyInfo InspectKeyInfo
err = json.Unmarshal(out, &keyInfo)
ss58Address, err := kyr.SS58Address(network)
if err != nil {
return KeyringPair{}, fmt.Errorf("failed to deserialize key info JSON output: %v", err.Error())
return KeyringPair{}, err
}

pk, err := hex.DecodeString(strings.Replace(keyInfo.PublicKey, "0x", "", 1))
if err != nil {
return KeyringPair{}, fmt.Errorf("failed to generate keyring pair from secret, could not hex decode pubkey: "+
"%v with error: %v", keyInfo.PublicKey, err.Error())
}
var pk = kyr.Public()

return KeyringPair{
URI: seedOrPhrase,
Address: keyInfo.SS58Address,
Address: ss58Address,
PublicKey: pk,
}, nil
}
Expand All @@ -103,31 +74,18 @@ func Sign(data []byte, privateKeyURI string) ([]byte, error) {
data = h[:]
}

// use "subkey" command for signature
cmd := exec.Command(subkeyCmd, "sign", "--suri", privateKeyURI, "--hex")

// data to stdin
dataHex := hex.EncodeToString(data)
cmd.Stdin = strings.NewReader(dataHex)

// log.Printf("echo -n \"%v\" | %v sign %v --hex", dataHex, subkeyCmd, privateKeyURI)

// execute the command, get the output
out, err := cmd.Output()
scheme := sr25519.Scheme{}
kyr, err := subkey.DeriveKeyPair(scheme, privateKeyURI)
if err != nil {
return nil, fmt.Errorf("failed to sign with subkey: %v", err.Error())
return nil, err
}

// remove line feed
if len(out) > 0 && out[len(out)-1] == 10 {
out = out[:len(out)-1]
signature, err := kyr.Sign(data)
if err != nil {
return nil, err
}

outStr := string(out)

dec, err := hex.DecodeString(outStr)

return dec, err
return signature, nil
}

// Verify verifies data using the provided signature and the key under the derivation path. Requires the subkey
Expand All @@ -139,32 +97,19 @@ func Verify(data []byte, sig []byte, privateKeyURI string) (bool, error) {
data = h[:]
}

// hexify the sig
sigHex := hex.EncodeToString(sig)

// use "subkey" command for signature
cmd := exec.Command(subkeyCmd, "verify", "--hex", sigHex, privateKeyURI)

// data to stdin
dataHex := hex.EncodeToString(data)
cmd.Stdin = strings.NewReader(dataHex)

//log.Printf("echo -n \"%v\" | %v verify --hex %v %v", dataHex, subkeyCmd, sigHex, privateKeyURI)

// execute the command, get the output
out, err := cmd.Output()
scheme := sr25519.Scheme{}
kyr, err := subkey.DeriveKeyPair(scheme, privateKeyURI)
if err != nil {
return false, fmt.Errorf("failed to verify with subkey: %v", err.Error())
return false, err
}

// remove line feed
if len(out) > 0 && out[len(out)-1] == 10 {
out = out[:len(out)-1]
if len(sig) != 64 {
return false, errors.New("wrong signature length")
}

outStr := string(out)
valid := outStr == "Signature verifies correctly."
return valid, nil
v := kyr.Verify(data, sig)

return v, nil
}

// LoadKeyringPairFromEnv looks up whether the env variable TEST_PRIV_KEY is set and is not empty and tries to use its
Expand All @@ -173,12 +118,18 @@ func Verify(data []byte, sig []byte, privateKeyURI string) (bool, error) {
// Loads Network from TEST_NETWORK variable
// Leave TEST_NETWORK empty or unset for default
func LoadKeyringPairFromEnv() (kp KeyringPair, ok bool) {
network := os.Getenv("TEST_NETWORK")
networkString := os.Getenv("TEST_NETWORK")
network, err := strconv.ParseInt(networkString, 10, 8)
if err != nil {
// defaults to generic substrate address
// https://github.com/paritytech/substrate/wiki/External-Address-Format-(SS58)#checksum-types
network = 42
}
priv, ok := os.LookupEnv("TEST_PRIV_KEY")
if !ok || priv == "" {
return kp, false
}
kp, err := KeyringPairFromSecret(priv, network)
kp, err = KeyringPairFromSecret(priv, uint8(network))
if err != nil {
panic(fmt.Errorf("cannot load keyring pair from env or use fallback: %v", err))
}
Expand Down
76 changes: 64 additions & 12 deletions signature/signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,36 +30,64 @@ var testSecretSeed = "0x167d9a020688544ea246b056799d6a771e97c9da057e4d0b87024537
var testPubKey = "0xdc64bef918ddda3126a39a11113767741ddfdf91399f055e1d963f2ae1ec2535"
var testAddressSS58 = "5H3gKVQU7DfNFfNGkgTrD7p715jjg7QXtat8X3UxiSyw7APW"
var testKusamaAddressSS58 = "HZHyokLjagJ1KBiXPGu75B79g1yUnDiLxisuhkvCFCRrWBk"
var testPolkadotAddressSS58 = "15yyTpfXxzvqhCNniKWrMGeFrhjPNQxfy5ccgLUKGY1THbTW"

func TestKeyRingPairFromSecretPhrase(t *testing.T) {
p, err := KeyringPairFromSecret(testSecretPhrase, "")
func TestKeyRingPairFromSecretPhrase_SubstrateAddress(t *testing.T) {
p, err := KeyringPairFromSecret(testSecretPhrase, 42)
assert.NoError(t, err)

assert.Equal(t, KeyringPair{
URI: testSecretPhrase,
Address: testAddressSS58,
URI: testSecretPhrase,
Address: testAddressSS58,
PublicKey: types.MustHexDecodeString(testPubKey),
}, p)
}

func TestKeyRingPairFromSecretPhrase_PolkadotAddress(t *testing.T) {
p, err := KeyringPairFromSecret(testSecretPhrase, 0)
assert.NoError(t, err)

assert.Equal(t, KeyringPair{
URI: testSecretPhrase,
Address: testPolkadotAddressSS58,
PublicKey: types.MustHexDecodeString(testPubKey),
}, p)
}

func TestKeyRingPairFromSecretPhrase_KusamaAddress(t *testing.T) {
p, err := KeyringPairFromSecret(testSecretPhrase, 2)
assert.NoError(t, err)

assert.Equal(t, KeyringPair{
URI: testSecretPhrase,
Address: testKusamaAddressSS58,
PublicKey: types.MustHexDecodeString(testPubKey),
}, p)
}

func TestKeyRingPairFromSecretPhrase_InvalidSecretPhrase(t *testing.T) {
_, err := KeyringPairFromSecret("foo", 42)
assert.Error(t, err)
}

func TestKeyringPairFromSecretSeed(t *testing.T) {
p, err := KeyringPairFromSecret(testSecretSeed, "")
p, err := KeyringPairFromSecret(testSecretSeed, 42)
assert.NoError(t, err)

assert.Equal(t, KeyringPair{
URI: testSecretSeed,
Address: testAddressSS58,
URI: testSecretSeed,
Address: testAddressSS58,
PublicKey: types.MustHexDecodeString(testPubKey),
}, p)
}

func TestKeyringPairFromSecretSeedAndNetwork(t *testing.T) {
p, err := KeyringPairFromSecret(testSecretSeed, "kusama")
p, err := KeyringPairFromSecret(testSecretSeed, 42)
assert.NoError(t, err)

assert.Equal(t, KeyringPair{
URI: testSecretSeed,
Address: testKusamaAddressSS58,
URI: testSecretSeed,
Address: testAddressSS58,
PublicKey: types.MustHexDecodeString(testPubKey),
}, p)
}
Expand All @@ -70,12 +98,36 @@ func TestSignAndVerify(t *testing.T) {
sig, err := Sign(data, TestKeyringPairAlice.URI)
assert.NoError(t, err)

ok, err := Verify(data, sig, types.HexEncodeToString(TestKeyringPairAlice.PublicKey))
ok, err := Verify(data, sig, TestKeyringPairAlice.URI)
assert.NoError(t, err)

assert.True(t, ok)
}

func TestSign_InvalidSecretPhrase(t *testing.T) {
data := []byte("hello!")

_, err := Sign(data, "foo")
assert.Error(t, err)
}

func TestSignAndVerify_InvalidSecretPhraseOnVerify(t *testing.T) {
data := []byte("hello!")

sig, err := Sign(data, TestKeyringPairAlice.URI)
assert.NoError(t, err)

_, err = Verify(data, sig, "foo")
assert.Error(t, err)
}

func TestVerify_InvalidSignatureLength(t *testing.T) {
data := []byte("hello!")

_, err := Verify(data, []byte{'f', 'o', 'o'}, TestKeyringPairAlice.URI)
assert.Error(t, err)
}

func TestSignAndVerifyLong(t *testing.T) {
data := make([]byte, 258)
_, err := rand.Read(data)
Expand All @@ -84,7 +136,7 @@ func TestSignAndVerifyLong(t *testing.T) {
sig, err := Sign(data, TestKeyringPairAlice.URI)
assert.NoError(t, err)

ok, err := Verify(data, sig, types.HexEncodeToString(TestKeyringPairAlice.PublicKey))
ok, err := Verify(data, sig, TestKeyringPairAlice.URI)
assert.NoError(t, err)

assert.True(t, ok)
Expand Down
2 changes: 1 addition & 1 deletion types/extrinsic_payload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestExtrinsicPayload_Sign(t *testing.T) {
// verify sig
b, err := EncodeToBytes(examplaryExtrinsicPayload)
assert.NoError(t, err)
ok, err := signature.Verify(b, sig[:], HexEncodeToString(signature.TestKeyringPairAlice.PublicKey))
ok, err := signature.Verify(b, sig[:], signature.TestKeyringPairAlice.URI)
assert.NoError(t, err)
assert.True(t, ok)
}
2 changes: 1 addition & 1 deletion types/extrinsic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func TestExtrinsic_Sign(t *testing.T) {
// verify sig
b, err := EncodeToBytes(verifyPayload)
assert.NoError(t, err)
ok, err := signature.Verify(b, extDec.Signature.Signature.AsSr25519[:], HexEncodeToString(signature.TestKeyringPairAlice.PublicKey))
ok, err := signature.Verify(b, extDec.Signature.Signature.AsSr25519[:], signature.TestKeyringPairAlice.URI)
assert.NoError(t, err)
assert.True(t, ok)
}
Expand Down

0 comments on commit f23fc78

Please sign in to comment.