133 lines
2.6 KiB
Go
133 lines
2.6 KiB
Go
package signer
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ed25519"
|
|
"crypto/hmac"
|
|
"crypto/sha512"
|
|
"encoding/binary"
|
|
"errors"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
FirstHardenedIndex = uint32(0x80000000)
|
|
seedModifier = "ed25519 seed"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidPath = errors.New("invalid derivation path")
|
|
ErrNoPublicDerivation = errors.New("no public derivation for ed25519")
|
|
|
|
pathRegex = regexp.MustCompile(`^m(\/[0-9]+')+$`)
|
|
)
|
|
|
|
type Key struct {
|
|
Key []byte
|
|
ChainCode []byte
|
|
}
|
|
|
|
// DeriveForPath derives key for a path in BIP-44 format and a seed.
|
|
// Ed25119 derivation operated on hardened keys only.
|
|
func DeriveForPath(path string, seed []byte) (*Key, error) {
|
|
if !isValidPath(path) {
|
|
return nil, ErrInvalidPath
|
|
}
|
|
|
|
key, err := NewMasterKey(seed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
segments := strings.Split(path, "/")
|
|
for _, segment := range segments[1:] {
|
|
i64, err := strconv.ParseUint(strings.TrimRight(segment, "'"), 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
i := uint32(i64) + FirstHardenedIndex
|
|
key, err = key.Derive(i)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// NewMasterKey generates a new master key from seed.
|
|
func NewMasterKey(seed []byte) (*Key, error) {
|
|
hash := hmac.New(sha512.New, []byte(seedModifier))
|
|
_, err := hash.Write(seed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sum := hash.Sum(nil)
|
|
key := &Key{
|
|
Key: sum[:32],
|
|
ChainCode: sum[32:],
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
func (k *Key) Derive(i uint32) (*Key, error) {
|
|
// no public derivation for ed25519
|
|
if i < FirstHardenedIndex {
|
|
return nil, ErrNoPublicDerivation
|
|
}
|
|
|
|
iBytes := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(iBytes, i)
|
|
key := append([]byte{0x0}, k.Key...)
|
|
data := append(key, iBytes...)
|
|
|
|
hash := hmac.New(sha512.New, k.ChainCode)
|
|
_, err := hash.Write(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sum := hash.Sum(nil)
|
|
newKey := &Key{
|
|
Key: sum[:32],
|
|
ChainCode: sum[32:],
|
|
}
|
|
return newKey, nil
|
|
}
|
|
|
|
// PublicKey returns public key for a derived private key.
|
|
func (k *Key) PublicKey() ([]byte, error) {
|
|
reader := bytes.NewReader(k.Key)
|
|
pub, _, err := ed25519.GenerateKey(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pub[:], nil
|
|
}
|
|
|
|
// RawSeed returns raw seed bytes
|
|
func (k *Key) RawSeed() [32]byte {
|
|
var rawSeed [32]byte
|
|
copy(rawSeed[:], k.Key[:])
|
|
return rawSeed
|
|
}
|
|
|
|
func isValidPath(path string) bool {
|
|
if !pathRegex.MatchString(path) {
|
|
return false
|
|
}
|
|
|
|
// Check for overflows
|
|
segments := strings.Split(path, "/")
|
|
for _, segment := range segments[1:] {
|
|
_, err := strconv.ParseUint(strings.TrimRight(segment, "'"), 10, 32)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|