aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--keys.go156
-rw-r--r--keys_test.go221
2 files changed, 377 insertions, 0 deletions
diff --git a/keys.go b/keys.go
new file mode 100644
index 0000000..10da93f
--- /dev/null
+++ b/keys.go
@@ -0,0 +1,156 @@
1package secp256k1
2
3import (
4 "crypto/rand"
5 "fmt"
6 "math/big"
7)
8
9// PrivateKey is a scalar (1 to N-1) used for signing
10type PrivateKey struct {
11 D *big.Int // the secret scalar
12}
13
14// PublicKey is a point on the curve (D * G)
15type PublicKey struct {
16 Point *Point
17}
18
19// GeneratePrivateKey creates a random private key
20func GeneratePrivateKey() (*PrivateKey, error) {
21 // Generate random bytes
22 bytes := make([]byte, 32)
23 _, err := rand.Read(bytes)
24 if err != nil {
25 return nil, fmt.Errorf("failed to generate random bytes: %w", err)
26 }
27
28 // Convert to big.Int and reduce mod N
29 d := new(big.Int).SetBytes(bytes)
30 d.Mod(d, N)
31
32 // Ensure it's not zero (extremely unlikely but must check)
33 if d.Sign() == 0 {
34 d.SetInt64(1)
35 }
36
37 return &PrivateKey{D: d}, nil
38}
39
40// NewPrivateKeyFromBytes creates a private key from 32 bytes
41func NewPrivateKeyFromBytes(b []byte) (*PrivateKey, error) {
42 if len(b) != 32 {
43 return nil, fmt.Errorf("private key must be 32 bytes, got %d", len(b))
44 }
45
46 d := new(big.Int).SetBytes(b)
47
48 // Validate: must be in range [1, N-1]
49 if d.Sign() == 0 {
50 return nil, fmt.Errorf("private key cannot be zero")
51 }
52 if d.Cmp(N) >= 0 {
53 return nil, fmt.Errorf("private key must be less than curve order N")
54 }
55
56 return &PrivateKey{D: d}, nil
57}
58
59// NewPrivateKeyFromHex creates a private key from a hex string
60func NewPrivateKeyFromHex(hex string) (*PrivateKey, error) {
61 d, ok := new(big.Int).SetString(hex, 16)
62 if !ok {
63 return nil, fmt.Errorf("invalid hex string")
64 }
65
66 // Validate range
67 if d.Sign() == 0 {
68 return nil, fmt.Errorf("private key cannot be zero")
69 }
70 if d.Cmp(N) >= 0 {
71 return nil, fmt.Errorf("private key must be less than curve order N")
72 }
73
74 return &PrivateKey{D: d}, nil
75}
76
77// PublicKey derives the public key from the private key
78// PublicKey = D * G
79func (priv *PrivateKey) PublicKey() *PublicKey {
80 point := G.ScalarMul(priv.D)
81 return &PublicKey{Point: point}
82}
83
84// Bytes returns the private key as 32 bytes (big-endian, zero-padded)
85func (priv *PrivateKey) Bytes() []byte {
86 b := priv.D.Bytes()
87 // Pad to 32 bytes
88 if len(b) < 32 {
89 padded := make([]byte, 32)
90 copy(padded[32-len(b):], b)
91 return padded
92 }
93 return b
94}
95
96// Hex returns the private key as a 64-character hex string
97func (priv *PrivateKey) Hex() string {
98 return fmt.Sprintf("%064x", priv.D)
99}
100
101// Bytes returns the public key in uncompressed format (65 bytes: 0x04 || x || y)
102func (pub *PublicKey) Bytes() []byte {
103 if pub.Point.IsInfinity() {
104 return []byte{0x00} // shouldn't happen with valid keys
105 }
106
107 result := make([]byte, 65)
108 result[0] = 0x04 // uncompressed prefix
109
110 xBytes := pub.Point.x.value.Bytes()
111 yBytes := pub.Point.y.value.Bytes()
112
113 // Copy x (padded to 32 bytes)
114 copy(result[1+(32-len(xBytes)):33], xBytes)
115 // Copy y (padded to 32 bytes)
116 copy(result[33+(32-len(yBytes)):65], yBytes)
117
118 return result
119}
120
121// BytesCompressed returns the public key in compressed format (33 bytes: prefix || x)
122// Prefix is 0x02 if y is even, 0x03 if y is odd
123func (pub *PublicKey) BytesCompressed() []byte {
124 if pub.Point.IsInfinity() {
125 return []byte{0x00}
126 }
127
128 result := make([]byte, 33)
129
130 // Prefix based on y parity
131 if pub.Point.y.value.Bit(0) == 0 {
132 result[0] = 0x02 // y is even
133 } else {
134 result[0] = 0x03 // y is odd
135 }
136
137 xBytes := pub.Point.x.value.Bytes()
138 copy(result[1+(32-len(xBytes)):33], xBytes)
139
140 return result
141}
142
143// Hex returns the public key as uncompressed hex (130 characters)
144func (pub *PublicKey) Hex() string {
145 return fmt.Sprintf("%x", pub.Bytes())
146}
147
148// HexCompressed returns the public key as compressed hex (66 characters)
149func (pub *PublicKey) HexCompressed() string {
150 return fmt.Sprintf("%x", pub.BytesCompressed())
151}
152
153// Equal checks if two public keys are the same
154func (pub *PublicKey) Equal(other *PublicKey) bool {
155 return pub.Point.Equal(other.Point)
156}
diff --git a/keys_test.go b/keys_test.go
new file mode 100644
index 0000000..e17ac98
--- /dev/null
+++ b/keys_test.go
@@ -0,0 +1,221 @@
1package secp256k1
2
3import (
4 "bytes"
5 "math/big"
6 "testing"
7)
8
9func TestGeneratePrivateKey(t *testing.T) {
10 priv, err := GeneratePrivateKey()
11 if err != nil {
12 t.Fatalf("failed to generate key: %v", err)
13 }
14
15 // Should be in valid range [1, N-1]
16 if priv.D.Sign() <= 0 {
17 t.Error("private key should be positive")
18 }
19 if priv.D.Cmp(N) >= 0 {
20 t.Error("private key should be less than N")
21 }
22}
23
24func TestGeneratePrivateKeyUnique(t *testing.T) {
25 // Generate two keys, they should be different
26 priv1, _ := GeneratePrivateKey()
27 priv2, _ := GeneratePrivateKey()
28
29 if priv1.D.Cmp(priv2.D) == 0 {
30 t.Error("two generated keys should not be identical")
31 }
32}
33
34func TestPrivateKeyFromBytes(t *testing.T) {
35 // 32 bytes of 0x01
36 b := make([]byte, 32)
37 b[31] = 0x01 // value = 1
38
39 priv, err := NewPrivateKeyFromBytes(b)
40 if err != nil {
41 t.Fatalf("failed to create key: %v", err)
42 }
43
44 if priv.D.Cmp(big.NewInt(1)) != 0 {
45 t.Errorf("expected D=1, got %s", priv.D.String())
46 }
47}
48
49func TestPrivateKeyFromBytesInvalidLength(t *testing.T) {
50 b := make([]byte, 31) // wrong length
51 _, err := NewPrivateKeyFromBytes(b)
52 if err == nil {
53 t.Error("should reject non-32-byte input")
54 }
55}
56
57func TestPrivateKeyFromBytesZero(t *testing.T) {
58 b := make([]byte, 32) // all zeros
59 _, err := NewPrivateKeyFromBytes(b)
60 if err == nil {
61 t.Error("should reject zero private key")
62 }
63}
64
65func TestPrivateKeyFromHex(t *testing.T) {
66 hex := "0000000000000000000000000000000000000000000000000000000000000001"
67 priv, err := NewPrivateKeyFromHex(hex)
68 if err != nil {
69 t.Fatalf("failed to create key: %v", err)
70 }
71
72 if priv.D.Cmp(big.NewInt(1)) != 0 {
73 t.Errorf("expected D=1, got %s", priv.D.String())
74 }
75}
76
77func TestPrivateKeyFromHexInvalid(t *testing.T) {
78 _, err := NewPrivateKeyFromHex("not-hex")
79 if err == nil {
80 t.Error("should reject invalid hex")
81 }
82}
83
84func TestPublicKeyDerivation(t *testing.T) {
85 // Private key = 1, public key should be G
86 priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001")
87 pub := priv.PublicKey()
88
89 if !pub.Point.Equal(G) {
90 t.Error("1 * G should equal G")
91 }
92}
93
94func TestPublicKeyDerivation2(t *testing.T) {
95 // Private key = 2, public key should be 2G
96 priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000002")
97 pub := priv.PublicKey()
98
99 expected := G.Double()
100 if !pub.Point.Equal(expected) {
101 t.Error("2 * G should equal 2G")
102 }
103}
104
105func TestPublicKeyOnCurve(t *testing.T) {
106 priv, _ := GeneratePrivateKey()
107 pub := priv.PublicKey()
108
109 if !pub.Point.IsOnCurve() {
110 t.Error("derived public key should be on curve")
111 }
112}
113
114func TestPrivateKeyBytes(t *testing.T) {
115 priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001")
116 b := priv.Bytes()
117
118 if len(b) != 32 {
119 t.Errorf("expected 32 bytes, got %d", len(b))
120 }
121 if b[31] != 0x01 {
122 t.Errorf("expected last byte to be 0x01, got 0x%02x", b[31])
123 }
124}
125
126func TestPrivateKeyRoundTrip(t *testing.T) {
127 priv1, _ := GeneratePrivateKey()
128 b := priv1.Bytes()
129 priv2, _ := NewPrivateKeyFromBytes(b)
130
131 if priv1.D.Cmp(priv2.D) != 0 {
132 t.Error("private key should survive bytes round-trip")
133 }
134}
135
136func TestPublicKeyBytesUncompressed(t *testing.T) {
137 priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001")
138 pub := priv.PublicKey()
139 b := pub.Bytes()
140
141 if len(b) != 65 {
142 t.Errorf("uncompressed pubkey should be 65 bytes, got %d", len(b))
143 }
144 if b[0] != 0x04 {
145 t.Errorf("uncompressed prefix should be 0x04, got 0x%02x", b[0])
146 }
147}
148
149func TestPublicKeyBytesCompressed(t *testing.T) {
150 priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001")
151 pub := priv.PublicKey()
152 b := pub.BytesCompressed()
153
154 if len(b) != 33 {
155 t.Errorf("compressed pubkey should be 33 bytes, got %d", len(b))
156 }
157 // G has odd y, so prefix should be 0x03
158 if b[0] != 0x02 && b[0] != 0x03 {
159 t.Errorf("compressed prefix should be 0x02 or 0x03, got 0x%02x", b[0])
160 }
161}
162
163func TestPublicKeyHex(t *testing.T) {
164 priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001")
165 pub := priv.PublicKey()
166
167 hex := pub.Hex()
168 if len(hex) != 130 { // 65 bytes * 2
169 t.Errorf("uncompressed hex should be 130 chars, got %d", len(hex))
170 }
171
172 hexComp := pub.HexCompressed()
173 if len(hexComp) != 66 { // 33 bytes * 2
174 t.Errorf("compressed hex should be 66 chars, got %d", len(hexComp))
175 }
176}
177
178func TestPublicKeyEqual(t *testing.T) {
179 priv1, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001")
180 priv2, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001")
181 priv3, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000002")
182
183 pub1 := priv1.PublicKey()
184 pub2 := priv2.PublicKey()
185 pub3 := priv3.PublicKey()
186
187 if !pub1.Equal(pub2) {
188 t.Error("same private key should produce equal public keys")
189 }
190 if pub1.Equal(pub3) {
191 t.Error("different private keys should produce different public keys")
192 }
193}
194
195// Known test vector from Bitcoin wiki
196func TestKnownKeyVector(t *testing.T) {
197 // Private key = 1
198 priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001")
199 pub := priv.PublicKey()
200
201 // Public key should be G
202 expectedX := Gx
203 expectedY := Gy
204
205 if pub.Point.x.value.Cmp(expectedX) != 0 {
206 t.Error("public key x doesn't match G.x")
207 }
208 if pub.Point.y.value.Cmp(expectedY) != 0 {
209 t.Error("public key y doesn't match G.y")
210 }
211
212 // Check compressed format starts with correct x
213 compressed := pub.BytesCompressed()
214 xFromCompressed := compressed[1:33]
215 expectedXBytes := make([]byte, 32)
216 copy(expectedXBytes[32-len(expectedX.Bytes()):], expectedX.Bytes())
217
218 if !bytes.Equal(xFromCompressed, expectedXBytes) {
219 t.Error("compressed pubkey x doesn't match")
220 }
221}