From e5fa7c1a85e9dd44ee92cb5da1797c82a0268fdb Mon Sep 17 00:00:00 2001 From: Clawd Date: Thu, 19 Feb 2026 21:08:14 -0800 Subject: Add key generation and serialization --- keys.go | 156 +++++++++++++++++++++++++++++++++++++++++ keys_test.go | 221 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 377 insertions(+) create mode 100644 keys.go create mode 100644 keys_test.go diff --git a/keys.go b/keys.go new file mode 100644 index 0000000..10da93f --- /dev/null +++ b/keys.go @@ -0,0 +1,156 @@ +package secp256k1 + +import ( + "crypto/rand" + "fmt" + "math/big" +) + +// PrivateKey is a scalar (1 to N-1) used for signing +type PrivateKey struct { + D *big.Int // the secret scalar +} + +// PublicKey is a point on the curve (D * G) +type PublicKey struct { + Point *Point +} + +// GeneratePrivateKey creates a random private key +func GeneratePrivateKey() (*PrivateKey, error) { + // Generate random bytes + bytes := make([]byte, 32) + _, err := rand.Read(bytes) + if err != nil { + return nil, fmt.Errorf("failed to generate random bytes: %w", err) + } + + // Convert to big.Int and reduce mod N + d := new(big.Int).SetBytes(bytes) + d.Mod(d, N) + + // Ensure it's not zero (extremely unlikely but must check) + if d.Sign() == 0 { + d.SetInt64(1) + } + + return &PrivateKey{D: d}, nil +} + +// NewPrivateKeyFromBytes creates a private key from 32 bytes +func NewPrivateKeyFromBytes(b []byte) (*PrivateKey, error) { + if len(b) != 32 { + return nil, fmt.Errorf("private key must be 32 bytes, got %d", len(b)) + } + + d := new(big.Int).SetBytes(b) + + // Validate: must be in range [1, N-1] + if d.Sign() == 0 { + return nil, fmt.Errorf("private key cannot be zero") + } + if d.Cmp(N) >= 0 { + return nil, fmt.Errorf("private key must be less than curve order N") + } + + return &PrivateKey{D: d}, nil +} + +// NewPrivateKeyFromHex creates a private key from a hex string +func NewPrivateKeyFromHex(hex string) (*PrivateKey, error) { + d, ok := new(big.Int).SetString(hex, 16) + if !ok { + return nil, fmt.Errorf("invalid hex string") + } + + // Validate range + if d.Sign() == 0 { + return nil, fmt.Errorf("private key cannot be zero") + } + if d.Cmp(N) >= 0 { + return nil, fmt.Errorf("private key must be less than curve order N") + } + + return &PrivateKey{D: d}, nil +} + +// PublicKey derives the public key from the private key +// PublicKey = D * G +func (priv *PrivateKey) PublicKey() *PublicKey { + point := G.ScalarMul(priv.D) + return &PublicKey{Point: point} +} + +// Bytes returns the private key as 32 bytes (big-endian, zero-padded) +func (priv *PrivateKey) Bytes() []byte { + b := priv.D.Bytes() + // Pad to 32 bytes + if len(b) < 32 { + padded := make([]byte, 32) + copy(padded[32-len(b):], b) + return padded + } + return b +} + +// Hex returns the private key as a 64-character hex string +func (priv *PrivateKey) Hex() string { + return fmt.Sprintf("%064x", priv.D) +} + +// Bytes returns the public key in uncompressed format (65 bytes: 0x04 || x || y) +func (pub *PublicKey) Bytes() []byte { + if pub.Point.IsInfinity() { + return []byte{0x00} // shouldn't happen with valid keys + } + + result := make([]byte, 65) + result[0] = 0x04 // uncompressed prefix + + xBytes := pub.Point.x.value.Bytes() + yBytes := pub.Point.y.value.Bytes() + + // Copy x (padded to 32 bytes) + copy(result[1+(32-len(xBytes)):33], xBytes) + // Copy y (padded to 32 bytes) + copy(result[33+(32-len(yBytes)):65], yBytes) + + return result +} + +// BytesCompressed returns the public key in compressed format (33 bytes: prefix || x) +// Prefix is 0x02 if y is even, 0x03 if y is odd +func (pub *PublicKey) BytesCompressed() []byte { + if pub.Point.IsInfinity() { + return []byte{0x00} + } + + result := make([]byte, 33) + + // Prefix based on y parity + if pub.Point.y.value.Bit(0) == 0 { + result[0] = 0x02 // y is even + } else { + result[0] = 0x03 // y is odd + } + + xBytes := pub.Point.x.value.Bytes() + copy(result[1+(32-len(xBytes)):33], xBytes) + + return result +} + +// Hex returns the public key as uncompressed hex (130 characters) +func (pub *PublicKey) Hex() string { + return fmt.Sprintf("%x", pub.Bytes()) +} + +// HexCompressed returns the public key as compressed hex (66 characters) +func (pub *PublicKey) HexCompressed() string { + return fmt.Sprintf("%x", pub.BytesCompressed()) +} + +// Equal checks if two public keys are the same +func (pub *PublicKey) Equal(other *PublicKey) bool { + return pub.Point.Equal(other.Point) +} 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 @@ +package secp256k1 + +import ( + "bytes" + "math/big" + "testing" +) + +func TestGeneratePrivateKey(t *testing.T) { + priv, err := GeneratePrivateKey() + if err != nil { + t.Fatalf("failed to generate key: %v", err) + } + + // Should be in valid range [1, N-1] + if priv.D.Sign() <= 0 { + t.Error("private key should be positive") + } + if priv.D.Cmp(N) >= 0 { + t.Error("private key should be less than N") + } +} + +func TestGeneratePrivateKeyUnique(t *testing.T) { + // Generate two keys, they should be different + priv1, _ := GeneratePrivateKey() + priv2, _ := GeneratePrivateKey() + + if priv1.D.Cmp(priv2.D) == 0 { + t.Error("two generated keys should not be identical") + } +} + +func TestPrivateKeyFromBytes(t *testing.T) { + // 32 bytes of 0x01 + b := make([]byte, 32) + b[31] = 0x01 // value = 1 + + priv, err := NewPrivateKeyFromBytes(b) + if err != nil { + t.Fatalf("failed to create key: %v", err) + } + + if priv.D.Cmp(big.NewInt(1)) != 0 { + t.Errorf("expected D=1, got %s", priv.D.String()) + } +} + +func TestPrivateKeyFromBytesInvalidLength(t *testing.T) { + b := make([]byte, 31) // wrong length + _, err := NewPrivateKeyFromBytes(b) + if err == nil { + t.Error("should reject non-32-byte input") + } +} + +func TestPrivateKeyFromBytesZero(t *testing.T) { + b := make([]byte, 32) // all zeros + _, err := NewPrivateKeyFromBytes(b) + if err == nil { + t.Error("should reject zero private key") + } +} + +func TestPrivateKeyFromHex(t *testing.T) { + hex := "0000000000000000000000000000000000000000000000000000000000000001" + priv, err := NewPrivateKeyFromHex(hex) + if err != nil { + t.Fatalf("failed to create key: %v", err) + } + + if priv.D.Cmp(big.NewInt(1)) != 0 { + t.Errorf("expected D=1, got %s", priv.D.String()) + } +} + +func TestPrivateKeyFromHexInvalid(t *testing.T) { + _, err := NewPrivateKeyFromHex("not-hex") + if err == nil { + t.Error("should reject invalid hex") + } +} + +func TestPublicKeyDerivation(t *testing.T) { + // Private key = 1, public key should be G + priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + pub := priv.PublicKey() + + if !pub.Point.Equal(G) { + t.Error("1 * G should equal G") + } +} + +func TestPublicKeyDerivation2(t *testing.T) { + // Private key = 2, public key should be 2G + priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000002") + pub := priv.PublicKey() + + expected := G.Double() + if !pub.Point.Equal(expected) { + t.Error("2 * G should equal 2G") + } +} + +func TestPublicKeyOnCurve(t *testing.T) { + priv, _ := GeneratePrivateKey() + pub := priv.PublicKey() + + if !pub.Point.IsOnCurve() { + t.Error("derived public key should be on curve") + } +} + +func TestPrivateKeyBytes(t *testing.T) { + priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + b := priv.Bytes() + + if len(b) != 32 { + t.Errorf("expected 32 bytes, got %d", len(b)) + } + if b[31] != 0x01 { + t.Errorf("expected last byte to be 0x01, got 0x%02x", b[31]) + } +} + +func TestPrivateKeyRoundTrip(t *testing.T) { + priv1, _ := GeneratePrivateKey() + b := priv1.Bytes() + priv2, _ := NewPrivateKeyFromBytes(b) + + if priv1.D.Cmp(priv2.D) != 0 { + t.Error("private key should survive bytes round-trip") + } +} + +func TestPublicKeyBytesUncompressed(t *testing.T) { + priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + pub := priv.PublicKey() + b := pub.Bytes() + + if len(b) != 65 { + t.Errorf("uncompressed pubkey should be 65 bytes, got %d", len(b)) + } + if b[0] != 0x04 { + t.Errorf("uncompressed prefix should be 0x04, got 0x%02x", b[0]) + } +} + +func TestPublicKeyBytesCompressed(t *testing.T) { + priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + pub := priv.PublicKey() + b := pub.BytesCompressed() + + if len(b) != 33 { + t.Errorf("compressed pubkey should be 33 bytes, got %d", len(b)) + } + // G has odd y, so prefix should be 0x03 + if b[0] != 0x02 && b[0] != 0x03 { + t.Errorf("compressed prefix should be 0x02 or 0x03, got 0x%02x", b[0]) + } +} + +func TestPublicKeyHex(t *testing.T) { + priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + pub := priv.PublicKey() + + hex := pub.Hex() + if len(hex) != 130 { // 65 bytes * 2 + t.Errorf("uncompressed hex should be 130 chars, got %d", len(hex)) + } + + hexComp := pub.HexCompressed() + if len(hexComp) != 66 { // 33 bytes * 2 + t.Errorf("compressed hex should be 66 chars, got %d", len(hexComp)) + } +} + +func TestPublicKeyEqual(t *testing.T) { + priv1, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + priv2, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + priv3, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000002") + + pub1 := priv1.PublicKey() + pub2 := priv2.PublicKey() + pub3 := priv3.PublicKey() + + if !pub1.Equal(pub2) { + t.Error("same private key should produce equal public keys") + } + if pub1.Equal(pub3) { + t.Error("different private keys should produce different public keys") + } +} + +// Known test vector from Bitcoin wiki +func TestKnownKeyVector(t *testing.T) { + // Private key = 1 + priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + pub := priv.PublicKey() + + // Public key should be G + expectedX := Gx + expectedY := Gy + + if pub.Point.x.value.Cmp(expectedX) != 0 { + t.Error("public key x doesn't match G.x") + } + if pub.Point.y.value.Cmp(expectedY) != 0 { + t.Error("public key y doesn't match G.y") + } + + // Check compressed format starts with correct x + compressed := pub.BytesCompressed() + xFromCompressed := compressed[1:33] + expectedXBytes := make([]byte, 32) + copy(expectedXBytes[32-len(expectedX.Bytes()):], expectedX.Bytes()) + + if !bytes.Equal(xFromCompressed, expectedXBytes) { + t.Error("compressed pubkey x doesn't match") + } +} -- cgit v1.2.3