diff --git a/wallet/file/sr25519.go b/wallet/file/sr25519.go index 4bb27b73..9b2b607c 100644 --- a/wallet/file/sr25519.go +++ b/wallet/file/sr25519.go @@ -1,14 +1,20 @@ 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" + "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" - "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature/sr25519" - "github.com/tyler-smith/go-bip39" + sdkSr25519 "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature/sr25519" ) // Sr25519FromMnemonic derives a signer using ADR-8 from given mnemonic. @@ -27,7 +33,7 @@ func Sr25519FromMnemonic(mnemonic string, number uint32) (sdkSignature.Signer, e seed := bip39.NewSeed(mnemonic, "") - signer, chainCode, err := slip10.NewMasterKey(seed) + _, chainCode, skBinary, err := newMasterKey(seed) if err != nil { return nil, fmt.Errorf("sakg: error deriving master key: %w", err) } @@ -38,16 +44,76 @@ func Sr25519FromMnemonic(mnemonic string, number uint32) (sdkSignature.Signer, e return nil, fmt.Errorf("sakg: error creating BIP-0032 path %s: %w", pathStr, err) } + var signer sdkSignature.Signer for _, index := range path { - signer, chainCode, err = slip10.NewChildKey(signer, chainCode, index) + signer, chainCode, skBinary, err = newChildKey(skBinary, chainCode, index) if err != nil { return nil, fmt.Errorf("sakg: error deriving child key: %w", err) } } - sr25519signer, err := sr25519.NewSigner(signer.(signature.UnsafeSigner).UnsafeBytes()) + 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, err + return nil, chainCode, nil, err } - return sr25519signer, nil + sk := msk.ExpandUniform() + + signer := sdkSr25519.NewSignerFromKeyPair(sk.KeyPair()) + copy(chainCode[:], IR) + + return signer, chainCode, edSk[:], nil } diff --git a/wallet/file/sr25519_test.go b/wallet/file/sr25519_test.go new file mode 100644 index 00000000..94fcef45 --- /dev/null +++ b/wallet/file/sr25519_test.go @@ -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: "CPGPz5KFeFkNlx7rkKwyfCCof1kghlY15Gf6yUzrMxM=", valid: true}, + {mnemonic: "equip will roof matter pink blind book anxiety banner elbow sun young", num: 2, pubkey: "DHym/Mo3e1nCMclw/eSSlU4OWf+h2ePpkrD3HR8kWzY=", valid: true}, + {mnemonic: "equip will roof matter pink blind book anxiety banner elbow sun young", num: 3, pubkey: "hhaCSH9yzK9HxuiErQpahQKMatAVXALdE215qcgd9DM=", 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) + } + } +}