Skip to content

Commit

Permalink
feat(wallet): Add file-based sr25519-adr8 support
Browse files Browse the repository at this point in the history
  • Loading branch information
matevz committed Sep 26, 2023
1 parent 517ad46 commit e227b92
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 4 deletions.
20 changes: 16 additions & 4 deletions wallet/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ const (
// SupportedAlgorithmsForImport returns the algorithms supported by the given import kind.
func SupportedAlgorithmsForImport(kind *wallet.ImportKind) []string {
if kind == nil {
return []string{wallet.AlgorithmEd25519Adr8, wallet.AlgorithmEd25519Raw, wallet.AlgorithmSecp256k1Bip44, wallet.AlgorithmSecp256k1Raw, wallet.AlgorithmSr25519Raw}
return []string{wallet.AlgorithmEd25519Adr8, wallet.AlgorithmEd25519Raw, wallet.AlgorithmSecp256k1Bip44, wallet.AlgorithmSecp256k1Raw, wallet.AlgorithmSr25519Adr8, wallet.AlgorithmSr25519Raw}
}

switch *kind {
case wallet.ImportKindMnemonic:
return []string{wallet.AlgorithmEd25519Adr8, wallet.AlgorithmSecp256k1Bip44}
return []string{wallet.AlgorithmEd25519Adr8, wallet.AlgorithmSecp256k1Bip44, wallet.AlgorithmSr25519Adr8}
case wallet.ImportKindPrivateKey:
return []string{wallet.AlgorithmEd25519Raw, wallet.AlgorithmSecp256k1Raw, wallet.AlgorithmSr25519Raw}
default:
Expand Down Expand Up @@ -192,7 +192,7 @@ func (af *fileAccountFactory) PrettyKind(rawCfg map[string]interface{}) string {
// In case of ADR8 or BIP44 show the keypair number.
var number string
switch cfg.Algorithm {
case wallet.AlgorithmEd25519Adr8, wallet.AlgorithmSecp256k1Bip44:
case wallet.AlgorithmEd25519Adr8, wallet.AlgorithmSecp256k1Bip44, wallet.AlgorithmSr25519Adr8:
number = fmt.Sprintf(":%d", cfg.Number)
}
return fmt.Sprintf("%s (%s%s)", Kind, cfg.Algorithm, number)
Expand Down Expand Up @@ -429,7 +429,7 @@ func (af *fileAccountFactory) Import(name string, passphrase string, rawCfg map[
switch src.Kind {
case wallet.ImportKindMnemonic:
switch cfg.Algorithm {
case wallet.AlgorithmEd25519Adr8, wallet.AlgorithmSecp256k1Bip44:
case wallet.AlgorithmEd25519Adr8, wallet.AlgorithmSecp256k1Bip44, wallet.AlgorithmSr25519Adr8:
default:
return nil, fmt.Errorf("algorithm '%s' does not support import from mnemonic", cfg.Algorithm)
}
Expand Down Expand Up @@ -520,6 +520,18 @@ func newAccount(state *secretState, cfg *accountConfig) (wallet.Account, error)
return nil, fmt.Errorf("failed to initialize signer: %w", err)
}

return &fileAccount{
cfg: cfg,
state: state,
signer: signer,
}, nil
case wallet.AlgorithmSr25519Adr8:
// For Sr25519 use the ADR 0008 derivation scheme.
signer, err := Sr25519FromMnemonic(state.Data, cfg.Number)
if err != nil {
return nil, fmt.Errorf("failed to initialize signer: %w", err)
}

return &fileAccount{
cfg: cfg,
state: state,
Expand Down
119 changes: 119 additions & 0 deletions wallet/file/sr25519.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package file

import (
"crypto/hmac"
"crypto/sha512"
"encoding/binary"
"fmt"

"github.com/tyler-smith/go-bip39"

"github.com/oasisprotocol/curve25519-voi/primitives/ed25519"
"github.com/oasisprotocol/curve25519-voi/primitives/sr25519"
"github.com/oasisprotocol/oasis-core/go/common/crypto/sakg"
"github.com/oasisprotocol/oasis-core/go/common/crypto/signature/signers/memory"
"github.com/oasisprotocol/oasis-core/go/common/crypto/slip10"
sdkSignature "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature"
sdkSr25519 "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature/sr25519"
)

// Sr25519FromMnemonic derives a signer using ADR-8 from given mnemonic.
func Sr25519FromMnemonic(mnemonic string, number uint32) (sdkSignature.Signer, error) {
if number > sakg.MaxAccountKeyNumber {
return nil, fmt.Errorf(
"sakg: invalid key number: %d (maximum: %d)",
number,
sakg.MaxAccountKeyNumber,
)
}

if !bip39.IsMnemonicValid(mnemonic) {
return nil, fmt.Errorf("sakg: invalid mnemonic")
}

seed := bip39.NewSeed(mnemonic, "")

_, chainCode, skBinary, err := newMasterKey(seed)
if err != nil {
return nil, fmt.Errorf("sakg: error deriving master key: %w", err)
}

pathStr := fmt.Sprintf("%s/%d'", sakg.BIP32PathPrefix, number)
path, err := sakg.NewBIP32Path(pathStr)
if err != nil {
return nil, fmt.Errorf("sakg: error creating BIP-0032 path %s: %w", pathStr, err)
}

var signer sdkSignature.Signer
for _, index := range path {
signer, chainCode, skBinary, err = newChildKey(skBinary, chainCode, index)
if err != nil {
return nil, fmt.Errorf("sakg: error deriving child key: %w", err)
}
}

return signer, nil
}

func newMasterKey(seed []byte) (sdkSignature.Signer, slip10.ChainCode, []byte, error) {
// Let S be a seed byte sequence of 128 to 512 bits in length.
if sLen := len(seed); sLen < slip10.SeedMinSize || sLen > slip10.SeedMaxSize {
return nil, slip10.ChainCode{}, nil, fmt.Errorf("slip10: invalid seed")
}

// 1. Calculate I = HMAC-SHA512(Key = Curve, Data = S)
mac := hmac.New(sha512.New, []byte("ed25519 seed"))
_, _ = mac.Write(seed)
I := mac.Sum(nil)

// 2. Split I into two 32-byte sequences, IL and IR.
// 3. Use parse256(IL) as master secret key, and IR as master chain code.
return splitDigest(I)
}

func newChildKey(kPar []byte, cPar slip10.ChainCode, index uint32) (sdkSignature.Signer, slip10.ChainCode, []byte, error) { //nolint: revive
if len(kPar) < memory.SeedSize {
return nil, slip10.ChainCode{}, nil, fmt.Errorf("slip10: invalid parent key")
}

// 1. Check whether i >= 2^31 (whether the child is a hardened key).
if index < 1<<31 {
// If not (normal child):
// If curve is ed25519: return failure.
return nil, slip10.ChainCode{}, nil, fmt.Errorf("slip10: non-hardened keys not supported")
}

// If so (hardened child):
// let I = HMAC-SHA512(Key = cpar, Data = 0x00 || ser256(kpar) || ser32(i)).
// (Note: The 0x00 pads the private key to make it 33 bytes long.)
var b [4]byte
mac := hmac.New(sha512.New, cPar[:])
_, _ = mac.Write(b[0:1]) // 0x00
_, _ = mac.Write(kPar[:memory.SeedSize]) // ser256(kPar)
binary.BigEndian.PutUint32(b[:], index) // Note: The spec neglects to define ser32.
_, _ = mac.Write(b[:]) // ser32(i)
I := mac.Sum(nil)

// 2. Split I into two 32-byte sequences, IL and IR.
// 3. The returned chain code ci is IR.
// 4. If curve is ed25519: The returned child key ki is parse256(IL).
return splitDigest(I)
}

func splitDigest(digest []byte) (sdkSignature.Signer, slip10.ChainCode, []byte, error) {
IL, IR := digest[:32], digest[32:]

var chainCode slip10.ChainCode

edSk := ed25519.NewKeyFromSeed(IL) // Needed for the SLIP10 scheme.
msk, err := sr25519.NewMiniSecretKeyFromBytes(IL)
if err != nil {
return nil, chainCode, nil, err
}
sk := msk.ExpandUniform()

signer := sdkSr25519.NewSignerFromKeyPair(sk.KeyPair())
copy(chainCode[:], IR)

return signer, chainCode, edSk[:], nil
}
35 changes: 35 additions & 0 deletions wallet/file/sr25519_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package file

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestSr25519FromMnemonic(t *testing.T) {
mnemonics := []struct {
mnemonic string
num uint32
pubkey string
valid bool
}{
{mnemonic: "equip will roof matter pink blind book anxiety banner elbow sun young", num: 0, pubkey: "vP1R5MMzR/r9ricymYpPbBA9JvmlixdzlEYWEpx5ExY=", valid: true},
{mnemonic: "equip will roof matter pink blind book anxiety banner elbow sun young", num: 1, pubkey: "Ql96gaTGV7o0QzQbvPBSQfRT5K9PtBNLrxEh9kMV228=", valid: true},
{mnemonic: "equip will roof matter pink blind book anxiety banner elbow sun young", num: 2, pubkey: "qhbwbWt9k+3SW+68NxXq3knl2jokiGpc11DoKOrjTjo=", valid: true},
{mnemonic: "equip will roof matter pink blind book anxiety banner elbow sun young", num: 3, pubkey: "skCcMfU7D0olbxZtn8lLn0u41rJzxiCw7rcv3uVEw3Y=", valid: true},
{mnemonic: "actorr want explain gravity body drill bike update mask wool tell seven", pubkey: "", valid: false},
{mnemonic: "actor want explain gravity body drill bike update mask wool tell", pubkey: "", valid: false},
{mnemonic: "", pubkey: "", valid: false},
}

for _, m := range mnemonics {
if m.valid {
signer, err := Sr25519FromMnemonic(m.mnemonic, m.num)
require.NoError(t, err)
require.Equal(t, m.pubkey, signer.Public().String())
} else {
_, err := Sr25519FromMnemonic(m.mnemonic, 0)
require.Error(t, err)
}
}
}

0 comments on commit e227b92

Please sign in to comment.