Skip to content

Commit fa4b122

Browse files
robdefeoactions-user
authored andcommitted
feat: implement HD keys for secp256k1 (#742)
* feat: implement HD keys for secp256k1 * fix merge GitOrigin-RevId: 57793a5cf1cf6cd44c2e68d06841cfced67222cf
1 parent 081b30e commit fa4b122

14 files changed

+1352
-11
lines changed

cipher/nacl/nacl_test.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,30 @@ func Test_easySeal(t *testing.T) {
2424
wantErr bool
2525
}{
2626
{
27-
"success-bob",
27+
"success-2e322f...",
2828
args{
2929
[]byte("message"),
30-
ed25519test.BobPublicKey.Bytes(),
30+
[]byte{0x2e, 0x32, 0x2f, 0x87, 0x40, 0xc6, 0x1, 0x72, 0x11, 0x1a, 0xc8, 0xea, 0xdc, 0xdd, 0xa2, 0x51, 0x2f, 0x90, 0xd0, 0x6d, 0xe, 0x50, 0x3e, 0xf1, 0x89, 0x97, 0x9a, 0x15, 0x9b, 0xec, 0xe1, 0xe8},
3131
bytes.NewReader([]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")),
3232
},
3333
[]byte{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x5b, 0x19, 0x83, 0xe5, 0x6e, 0x7f, 0xed, 0xfe, 0xbb, 0xd0, 0x70, 0x34, 0xce, 0x25, 0x49, 0x76, 0xa3, 0x50, 0x78, 0x91, 0x18, 0xe6, 0xe3},
3434
false,
3535
},
3636
{
37-
"success-alice-sr25519",
37+
"success-169a11...",
3838
args{
3939
[]byte("message"),
40-
sr25519test.AlicePublicKey.Bytes(),
40+
[]byte{0x16, 0x9a, 0x11, 0x72, 0x18, 0x51, 0xf5, 0xdf, 0xf3, 0x54, 0x1d, 0xd5, 0xc4, 0xb0, 0xb4, 0x78, 0xac, 0x1c, 0xd0, 0x92, 0xc9, 0xd5, 0x97, 0x6e, 0x83, 0xda, 0xa0, 0xd0, 0x3f, 0x26, 0x62, 0xc},
4141
bytes.NewReader([]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")),
4242
},
4343
[]byte{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0xa3, 0x81, 0x27, 0x6f, 0xdb, 0x97, 0x17, 0x51, 0x10, 0x1e, 0x17, 0x2c, 0xec, 0x5b, 0xae, 0xdc, 0x7, 0x26, 0xea, 0x16, 0xe4, 0xc7, 0xde},
4444
false,
4545
},
4646
{
47-
"success-bob-sr25519",
47+
"success-84623e...",
4848
args{
4949
[]byte("message"),
50-
sr25519test.BobPublicKey.Bytes(),
50+
[]byte{0x84, 0x62, 0x3e, 0x72, 0x52, 0xe4, 0x11, 0x38, 0xaf, 0x69, 0x4, 0xe1, 0xb0, 0x23, 0x4, 0xc9, 0x41, 0x62, 0x5f, 0x39, 0xe5, 0x76, 0x25, 0x89, 0x12, 0x5d, 0xc1, 0xa2, 0xf2, 0xcf, 0x2e, 0x30},
5151
bytes.NewReader([]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")),
5252
},
5353
[]byte{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x6, 0x3e, 0xa1, 0x2c, 0xa5, 0x8e, 0x6b, 0x36, 0x98, 0xf8, 0x0, 0x56, 0x74, 0xfe, 0x32, 0x9a, 0x3e, 0x60, 0xf1, 0x1b, 0x53, 0x6d, 0x77},

factory/seed.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package factory
2+
3+
import (
4+
"crypto/rand"
5+
"io"
6+
)
7+
8+
// NewSeed returns a cryptographically secure random seed. Random seeds are needed
9+
// when creating a new secure private key.
10+
11+
// The length of the seed depends on it's usage. In most cases seed length is between
12+
// 16 and 64 making it (128 to 512 bits).
13+
func NewSeed(length uint8) ([]byte, error) {
14+
return generateSeed(rand.Reader, length)
15+
}
16+
17+
func generateSeed(r io.Reader, length uint8) ([]byte, error) {
18+
buf := make([]byte, length)
19+
20+
if _, err := r.Read(buf); err != nil {
21+
return nil, err
22+
}
23+
24+
return buf, nil
25+
}

factory/seed_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package factory
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"io"
7+
"testing"
8+
"testing/iotest"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_generateSeed(t *testing.T) {
14+
type args struct {
15+
r io.Reader
16+
length uint8
17+
}
18+
tests := []struct {
19+
name string
20+
args args
21+
want []byte
22+
wantErr bool
23+
}{
24+
{
25+
"success",
26+
args{
27+
bytes.NewReader([]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")),
28+
16,
29+
},
30+
[]byte{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50},
31+
false,
32+
},
33+
{
34+
"err",
35+
args{
36+
iotest.ErrReader(errors.New("error")),
37+
255,
38+
},
39+
nil,
40+
true,
41+
},
42+
}
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
got, err := generateSeed(tt.args.r, tt.args.length)
46+
if (err != nil) != tt.wantErr {
47+
t.Errorf("generateSeed() error = %v, wantErr %v", err, tt.wantErr)
48+
return
49+
}
50+
if !assert.Equal(t, tt.want, got) {
51+
t.Errorf("generateSeed() = %v, want %v", got, tt.want)
52+
}
53+
})
54+
}
55+
}

private.go

+7
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,10 @@ type PrivateKey interface {
2626
// Sign signs the message with the key and returns the signature.
2727
Sign(message []byte) ([]byte, error)
2828
}
29+
30+
type ExtendedPrivateKey interface {
31+
Bytes() []byte
32+
PrivateKey() PrivateKey
33+
Derive(index uint32) (ExtendedPrivateKey, error)
34+
ExtendedPublicKey() (ExtendedPublicKey, error)
35+
}

public.go

+6
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,9 @@ type PublicKey interface {
2626
// Verify verifies whether sig is a valid signature of message.
2727
Verify(message, sig []byte) bool
2828
}
29+
30+
type ExtendedPublicKey interface {
31+
Bytes() []byte
32+
PublicKey() PublicKey
33+
Derive(index uint32) (ExtendedPublicKey, error)
34+
}

secp256k1/extended_end_to_end_test.go

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package secp256k1_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/mailchain/mailchain/crypto"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestPrivatePublic_Derive(t *testing.T) {
11+
testVec1MasterPrivKey := "xprv9wNUHWVTuAHnj7y9JJRvdqgd8jsN5QuzdPt7EuBXfXXgjMEWPc5dENSs3HKvXvoPMyJsBpSMkEryBEz3kxdRg8fpAfq9RYh4wiysZihDR2r"
12+
testVec1MasterPubKey := "xpub6AMph22MjXr5wc3cQKxvzydMgmhrUsdqzcoi3Hb9Ds4fc9Zew9PsnAmLtaBNTZCtzsZfLMgBM6DEFZGX2A4kHWDatJj6cfbRH896d2ACi4F"
13+
testVec2MasterPrivKey := "xprv9wHokC2KXdTSpEepFcu53hMDUHYfAtTaLEJEMyxBPAMf78hJg17WhL5FyeDUQH5KWmGjGgEb2j74gsZqgupWpPbZgP6uFmP8MYEy5BNbyET"
14+
testVec2MasterPubKey := "xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU"
15+
16+
tests := []struct {
17+
name string
18+
privateKey crypto.ExtendedPrivateKey
19+
publicKey crypto.ExtendedPublicKey
20+
path []uint32
21+
wantErr bool
22+
}{
23+
{
24+
"test vector 1 chain m/0/1",
25+
privateKeyFromBIP32String(t, testVec1MasterPrivKey),
26+
publicKeyFromBIP32String(t, testVec1MasterPubKey),
27+
[]uint32{0, 1},
28+
false,
29+
},
30+
{
31+
"test vector 1 chain m",
32+
privateKeyFromBIP32String(t, testVec1MasterPrivKey),
33+
publicKeyFromBIP32String(t, testVec1MasterPubKey),
34+
[]uint32{},
35+
false,
36+
},
37+
{
38+
"test vector 1 chain m/0",
39+
privateKeyFromBIP32String(t, testVec1MasterPrivKey),
40+
publicKeyFromBIP32String(t, testVec1MasterPubKey),
41+
[]uint32{0},
42+
false,
43+
},
44+
{
45+
"test vector 1 chain m/0/1",
46+
privateKeyFromBIP32String(t, testVec1MasterPrivKey),
47+
publicKeyFromBIP32String(t, testVec1MasterPubKey),
48+
[]uint32{0, 1},
49+
false,
50+
},
51+
{
52+
"test vector 1 chain m/0/1/2",
53+
privateKeyFromBIP32String(t, testVec1MasterPrivKey),
54+
publicKeyFromBIP32String(t, testVec1MasterPubKey),
55+
[]uint32{0, 1, 2},
56+
false,
57+
},
58+
{
59+
"test vector 1 chain m/0/1/2/2",
60+
privateKeyFromBIP32String(t, testVec1MasterPrivKey),
61+
publicKeyFromBIP32String(t, testVec1MasterPubKey),
62+
[]uint32{0, 1, 2, 2},
63+
false,
64+
},
65+
{
66+
"test vector 1 chain m/0/1/2/2/1000000000",
67+
privateKeyFromBIP32String(t, testVec1MasterPrivKey),
68+
publicKeyFromBIP32String(t, testVec1MasterPubKey),
69+
[]uint32{0, 1, 2, 2, 1000000000},
70+
false,
71+
},
72+
73+
// // Test vector 2
74+
{
75+
"test vector 2 chain m",
76+
privateKeyFromBIP32String(t, testVec2MasterPrivKey),
77+
publicKeyFromBIP32String(t, testVec2MasterPubKey),
78+
[]uint32{},
79+
false,
80+
},
81+
{
82+
"test vector 2 chain m/0",
83+
privateKeyFromBIP32String(t, testVec2MasterPrivKey),
84+
publicKeyFromBIP32String(t, testVec2MasterPubKey),
85+
[]uint32{0},
86+
false,
87+
},
88+
{
89+
"test vector 2 chain m/0/2147483647",
90+
privateKeyFromBIP32String(t, testVec2MasterPrivKey),
91+
publicKeyFromBIP32String(t, testVec2MasterPubKey),
92+
[]uint32{0, 2147483647},
93+
false,
94+
},
95+
{
96+
"test vector 2 chain m/0/2147483647/1",
97+
privateKeyFromBIP32String(t, testVec2MasterPrivKey),
98+
publicKeyFromBIP32String(t, testVec2MasterPubKey),
99+
[]uint32{0, 2147483647, 1},
100+
false,
101+
},
102+
{
103+
"test vector 2 chain m/0/2147483647/1/2147483646",
104+
privateKeyFromBIP32String(t, testVec2MasterPrivKey),
105+
publicKeyFromBIP32String(t, testVec2MasterPubKey),
106+
[]uint32{0, 2147483647, 1, 2147483646},
107+
false,
108+
},
109+
{
110+
"test vector 2 chain m/0/2147483647/1/2147483646/2",
111+
privateKeyFromBIP32String(t, testVec2MasterPrivKey),
112+
publicKeyFromBIP32String(t, testVec2MasterPubKey),
113+
[]uint32{0, 2147483647, 1, 2147483646, 2},
114+
false,
115+
},
116+
}
117+
for _, tt := range tests {
118+
t.Run(tt.name, func(t *testing.T) {
119+
extPrvKey := tt.privateKey
120+
extPubKey := tt.publicKey
121+
var err error
122+
123+
for _, childNum := range tt.path {
124+
extPrvKey, err = extPrvKey.Derive(childNum)
125+
if !assert.NoError(t, err) {
126+
assert.FailNow(t, "error not expected")
127+
}
128+
}
129+
130+
for _, childNum := range tt.path {
131+
extPubKey, err = extPubKey.Derive(childNum)
132+
if !assert.NoError(t, err) {
133+
assert.FailNow(t, "error not expected")
134+
}
135+
}
136+
137+
pubKey, err := extPrvKey.ExtendedPublicKey()
138+
if !assert.NoError(t, err) {
139+
assert.FailNow(t, "error not expected")
140+
}
141+
142+
if !assert.Equal(t, pubKey, extPubKey) {
143+
t.Errorf("RoundTripParse and Derive = %v, want %v", pubKey.Bytes(), extPubKey.Bytes())
144+
}
145+
})
146+
}
147+
}

0 commit comments

Comments
 (0)