diff options
| author | Clawd <ai@clawd.bot> | 2026-02-20 18:52:08 -0800 |
|---|---|---|
| committer | Clawd <ai@clawd.bot> | 2026-02-20 18:52:08 -0800 |
| commit | d641f4566f051656bae79e406155c4f7f65ec338 (patch) | |
| tree | 4cd01f3fa4585cf719116a303473e792ea67e82a /keys.go | |
| parent | 84709fd67c02058334519ebee9c110f68b33d9b4 (diff) | |
embed secp256k1: replace btcec with internal pure-go implementation
This removes all external dependencies by embedding the secp256k1-learn
implementation into internal/secp256k1.
Changes:
- Add internal/secp256k1 with field arithmetic, curve ops, keys, schnorr
- Update keys.go to use internal secp256k1 package
- Remove btcec/btcutil dependencies (go.mod is now clean)
- All tests pass
Tradeoffs:
- ~10x slower crypto ops vs btcec (acceptable for nostr use case)
- Not constant-time (documented limitation)
- Zero external dependencies
Refs: code.northwest.io/secp256k1-learn
Diffstat (limited to 'keys.go')
| -rw-r--r-- | keys.go | 47 |
1 files changed, 21 insertions, 26 deletions
| @@ -1,34 +1,30 @@ | |||
| 1 | package nostr | 1 | package nostr |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "crypto/rand" | ||
| 5 | "encoding/hex" | 4 | "encoding/hex" |
| 6 | "fmt" | 5 | "fmt" |
| 7 | "strings" | 6 | "strings" |
| 8 | "time" | 7 | "time" |
| 9 | 8 | ||
| 10 | "github.com/btcsuite/btcd/btcec/v2" | 9 | "code.northwest.io/nostr/internal/secp256k1" |
| 11 | "github.com/btcsuite/btcd/btcec/v2/schnorr" | ||
| 12 | ) | 10 | ) |
| 13 | 11 | ||
| 14 | // Key represents a Nostr key, which may be a full private key or public-only. | 12 | // Key represents a Nostr key, which may be a full private key or public-only. |
| 15 | // Use GenerateKey or ParseKey for private keys, ParsePublicKey for public-only. | 13 | // Use GenerateKey or ParseKey for private keys, ParsePublicKey for public-only. |
| 16 | type Key struct { | 14 | type Key struct { |
| 17 | priv *btcec.PrivateKey // nil for public-only keys | 15 | priv *secp256k1.PrivateKey // nil for public-only keys |
| 18 | pub *btcec.PublicKey // always set | 16 | pub *secp256k1.PublicKey // always set |
| 19 | } | 17 | } |
| 20 | 18 | ||
| 21 | // GenerateKey generates a new random private key. | 19 | // GenerateKey generates a new random private key. |
| 22 | func GenerateKey() (*Key, error) { | 20 | func GenerateKey() (*Key, error) { |
| 23 | var keyBytes [32]byte | 21 | priv, err := secp256k1.GeneratePrivateKey() |
| 24 | if _, err := rand.Read(keyBytes[:]); err != nil { | 22 | if err != nil { |
| 25 | return nil, fmt.Errorf("failed to generate random bytes: %w", err) | 23 | return nil, fmt.Errorf("failed to generate key: %w", err) |
| 26 | } | 24 | } |
| 27 | |||
| 28 | priv, _ := btcec.PrivKeyFromBytes(keyBytes[:]) | ||
| 29 | return &Key{ | 25 | return &Key{ |
| 30 | priv: priv, | 26 | priv: priv, |
| 31 | pub: priv.PubKey(), | 27 | pub: priv.PublicKey(), |
| 32 | }, nil | 28 | }, nil |
| 33 | } | 29 | } |
| 34 | 30 | ||
| @@ -56,14 +52,14 @@ func ParseKey(s string) (*Key, error) { | |||
| 56 | } | 52 | } |
| 57 | } | 53 | } |
| 58 | 54 | ||
| 59 | if len(privBytes) != 32 { | 55 | priv, err := secp256k1.NewPrivateKeyFromBytes(privBytes) |
| 60 | return nil, fmt.Errorf("private key must be 32 bytes, got %d", len(privBytes)) | 56 | if err != nil { |
| 57 | return nil, fmt.Errorf("invalid private key: %w", err) | ||
| 61 | } | 58 | } |
| 62 | 59 | ||
| 63 | priv, _ := btcec.PrivKeyFromBytes(privBytes) | ||
| 64 | return &Key{ | 60 | return &Key{ |
| 65 | priv: priv, | 61 | priv: priv, |
| 66 | pub: priv.PubKey(), | 62 | pub: priv.PublicKey(), |
| 67 | }, nil | 63 | }, nil |
| 68 | } | 64 | } |
| 69 | 65 | ||
| @@ -96,7 +92,7 @@ func ParsePublicKey(s string) (*Key, error) { | |||
| 96 | return nil, fmt.Errorf("public key must be 32 bytes, got %d", len(pubBytes)) | 92 | return nil, fmt.Errorf("public key must be 32 bytes, got %d", len(pubBytes)) |
| 97 | } | 93 | } |
| 98 | 94 | ||
| 99 | pub, err := schnorr.ParsePubKey(pubBytes) | 95 | pub, err := secp256k1.ParsePublicKeyXOnly(pubBytes) |
| 100 | if err != nil { | 96 | if err != nil { |
| 101 | return nil, fmt.Errorf("invalid public key: %w", err) | 97 | return nil, fmt.Errorf("invalid public key: %w", err) |
| 102 | } | 98 | } |
| @@ -114,7 +110,7 @@ func (k *Key) CanSign() bool { | |||
| 114 | 110 | ||
| 115 | // Public returns the public key as a 64-character hex string. | 111 | // Public returns the public key as a 64-character hex string. |
| 116 | func (k *Key) Public() string { | 112 | func (k *Key) Public() string { |
| 117 | return hex.EncodeToString(schnorr.SerializePubKey(k.pub)) | 113 | return hex.EncodeToString(k.pub.XOnlyBytes()) |
| 118 | } | 114 | } |
| 119 | 115 | ||
| 120 | // Private returns the private key as a 64-character hex string. | 116 | // Private returns the private key as a 64-character hex string. |
| @@ -123,13 +119,12 @@ func (k *Key) Private() string { | |||
| 123 | if k.priv == nil { | 119 | if k.priv == nil { |
| 124 | return "" | 120 | return "" |
| 125 | } | 121 | } |
| 126 | return hex.EncodeToString(k.priv.Serialize()) | 122 | return hex.EncodeToString(k.priv.Bytes()) |
| 127 | } | 123 | } |
| 128 | 124 | ||
| 129 | // Npub returns the public key in bech32 npub format. | 125 | // Npub returns the public key in bech32 npub format. |
| 130 | func (k *Key) Npub() string { | 126 | func (k *Key) Npub() string { |
| 131 | pubBytes := schnorr.SerializePubKey(k.pub) | 127 | npub, _ := Bech32Encode("npub", k.pub.XOnlyBytes()) |
| 132 | npub, _ := Bech32Encode("npub", pubBytes) | ||
| 133 | return npub | 128 | return npub |
| 134 | } | 129 | } |
| 135 | 130 | ||
| @@ -139,7 +134,7 @@ func (k *Key) Nsec() string { | |||
| 139 | if k.priv == nil { | 134 | if k.priv == nil { |
| 140 | return "" | 135 | return "" |
| 141 | } | 136 | } |
| 142 | nsec, _ := Bech32Encode("nsec", k.priv.Serialize()) | 137 | nsec, _ := Bech32Encode("nsec", k.priv.Bytes()) |
| 143 | return nsec | 138 | return nsec |
| 144 | } | 139 | } |
| 145 | 140 | ||
| @@ -168,12 +163,12 @@ func (k *Key) Sign(event *Event) error { | |||
| 168 | } | 163 | } |
| 169 | 164 | ||
| 170 | // Sign with Schnorr | 165 | // Sign with Schnorr |
| 171 | sig, err := schnorr.Sign(k.priv, idBytes) | 166 | sig, err := secp256k1.Sign(k.priv, idBytes) |
| 172 | if err != nil { | 167 | if err != nil { |
| 173 | return fmt.Errorf("failed to sign event: %w", err) | 168 | return fmt.Errorf("failed to sign event: %w", err) |
| 174 | } | 169 | } |
| 175 | 170 | ||
| 176 | event.Sig = hex.EncodeToString(sig.Serialize()) | 171 | event.Sig = hex.EncodeToString(sig.Bytes()) |
| 177 | return nil | 172 | return nil |
| 178 | } | 173 | } |
| 179 | 174 | ||
| @@ -191,7 +186,7 @@ func (e *Event) Verify() bool { | |||
| 191 | return false | 186 | return false |
| 192 | } | 187 | } |
| 193 | 188 | ||
| 194 | pubKey, err := schnorr.ParsePubKey(pubKeyBytes) | 189 | pubKey, err := secp256k1.ParsePublicKeyXOnly(pubKeyBytes) |
| 195 | if err != nil { | 190 | if err != nil { |
| 196 | return false | 191 | return false |
| 197 | } | 192 | } |
| @@ -202,7 +197,7 @@ func (e *Event) Verify() bool { | |||
| 202 | return false | 197 | return false |
| 203 | } | 198 | } |
| 204 | 199 | ||
| 205 | sig, err := schnorr.ParseSignature(sigBytes) | 200 | sig, err := secp256k1.SignatureFromBytes(sigBytes) |
| 206 | if err != nil { | 201 | if err != nil { |
| 207 | return false | 202 | return false |
| 208 | } | 203 | } |
| @@ -213,5 +208,5 @@ func (e *Event) Verify() bool { | |||
| 213 | return false | 208 | return false |
| 214 | } | 209 | } |
| 215 | 210 | ||
| 216 | return sig.Verify(idBytes, pubKey) | 211 | return secp256k1.Verify(pubKey, idBytes, sig) |
| 217 | } | 212 | } |
