Elliptic Curve Cryptography
ECDSA signatures and key exchange with secp256k1, secp256r1, and bn256 curves
Elliptic Curve Cryptography
Lux implements multiple elliptic curves for digital signatures, key exchange, and advanced cryptographic protocols. This includes secp256k1 (Bitcoin/Ethereum), secp256r1 (P-256/NIST), and bn256 (pairing-friendly) curves.
Overview
Elliptic Curve Cryptography (ECC) provides equivalent security to RSA with much smaller key sizes:
- Compact Keys: 256-bit ECC ≈ 3072-bit RSA security
- Efficient Operations: Faster signing and verification
- Mobile Friendly: Lower computational and storage requirements
- Multiple Curves: Different curves for different use cases
Secp256k1
The secp256k1 curve is used by Bitcoin, Ethereum, and many other blockchains for digital signatures.
Curve Parameters
| Parameter | Value |
|---|---|
| Field | Prime field, p = 2^256 - 2^32 - 977 |
| Equation | y² = x³ + 7 |
| Generator Point | G = (0x79BE667E...F9DCBBAC, 0x483ADA77...FED9CB5) |
| Order | n = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141 |
| Cofactor | h = 1 |
| Security | 128-bit |
Implementation
package main
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"math/big"
"github.com/luxfi/crypto/secp256k1"
)
// Generate secp256k1 key pair
func generateSecp256k1Keys() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(secp256k1.S256(), rand.Reader)
}
// Sign with secp256k1
func signSecp256k1(privateKey *ecdsa.PrivateKey, message []byte) ([]byte, error) {
// Hash the message
hash := sha256.Sum256(message)
// Sign the hash
r, s, err := ecdsa.Sign(rand.Reader, privateKey, hash[:])
if err != nil {
return nil, err
}
// Encode signature (DER format)
signature := append(r.Bytes(), s.Bytes()...)
return signature, nil
}
// Verify secp256k1 signature
func verifySecp256k1(publicKey *ecdsa.PublicKey, message, signature []byte) bool {
hash := sha256.Sum256(message)
// Decode signature
r := new(big.Int).SetBytes(signature[:32])
s := new(big.Int).SetBytes(signature[32:])
return ecdsa.Verify(publicKey, hash[:], r, s)
}
// Recover public key from signature (Ethereum-style)
func recoverPublicKey(hash, signature []byte, recovery byte) (*ecdsa.PublicKey, error) {
// Ensure hash is 32 bytes
if len(hash) != 32 {
return nil, fmt.Errorf("hash must be 32 bytes")
}
// Recover the public key
pubKey, err := secp256k1.RecoverPubkey(hash, append(signature, recovery))
if err != nil {
return nil, err
}
// Parse the recovered public key
x, y := secp256k1.DecompressPubkey(pubKey)
return &ecdsa.PublicKey{
Curve: secp256k1.S256(),
X: x,
Y: y,
}, nil
}Address Generation
Generate Bitcoin and Ethereum addresses from secp256k1 keys:
// Bitcoin address generation (P2PKH)
func bitcoinAddress(publicKey *ecdsa.PublicKey) string {
// Serialize public key (compressed)
pubKeyBytes := secp256k1.CompressPubkey(publicKey.X, publicKey.Y)
// SHA-256
sha256Hash := sha256.Sum256(pubKeyBytes)
// RIPEMD-160
ripemd160 := ripemd160.New()
ripemd160.Write(sha256Hash[:])
pubKeyHash := ripemd160.Sum(nil)
// Add version byte (0x00 for mainnet)
versionedHash := append([]byte{0x00}, pubKeyHash...)
// Double SHA-256 for checksum
checksum := sha256.Sum256(versionedHash)
checksum = sha256.Sum256(checksum[:])
// Append first 4 bytes of checksum
address := append(versionedHash, checksum[:4]...)
// Base58 encode
return base58.Encode(address)
}
// Ethereum address generation
func ethereumAddress(publicKey *ecdsa.PublicKey) string {
// Serialize public key (uncompressed, without 0x04 prefix)
pubKeyBytes := append(publicKey.X.Bytes(), publicKey.Y.Bytes()...)
// Keccak-256 hash
hash := sha3.NewLegacyKeccak256()
hash.Write(pubKeyBytes)
addressBytes := hash.Sum(nil)
// Take last 20 bytes
return "0x" + hex.EncodeToString(addressBytes[12:])
}Schnorr Signatures
Implement Schnorr signatures on secp256k1 (BIP-340):
// Schnorr signature implementation
type SchnorrSignature struct {
R *big.Int // 32 bytes
S *big.Int // 32 bytes
}
func schnorrSign(privateKey *big.Int, message []byte) *SchnorrSignature {
curve := secp256k1.S256()
// Generate nonce deterministically (RFC 6979)
k := deterministicNonce(privateKey, message)
// R = k*G
rx, ry := curve.ScalarBaseMult(k.Bytes())
// If ry is odd, negate k
if ry.Bit(0) == 1 {
k.Sub(curve.Params().N, k)
}
// e = H(R.x || P || m)
px, _ := curve.ScalarBaseMult(privateKey.Bytes())
e := schnorrChallenge(rx, px, message)
// s = k + e*x
s := new(big.Int).Mul(e, privateKey)
s.Add(s, k)
s.Mod(s, curve.Params().N)
return &SchnorrSignature{R: rx, S: s}
}
func schnorrVerify(publicKey *ecdsa.PublicKey, message []byte, sig *SchnorrSignature) bool {
curve := secp256k1.S256()
// e = H(R.x || P || m)
e := schnorrChallenge(sig.R, publicKey.X, message)
// Verify: s*G = R + e*P
sx, sy := curve.ScalarBaseMult(sig.S.Bytes())
ex, ey := curve.ScalarMult(publicKey.X, publicKey.Y, e.Bytes())
rx, ry := curve.Add(sig.R, big.NewInt(0), ex, new(big.Int).Neg(ey))
return sx.Cmp(rx) == 0 && sy.Cmp(ry) == 0
}Secp256r1 (P-256)
The secp256r1 curve (also known as P-256 or prime256v1) is a NIST-standardized curve widely used in TLS and secure hardware.
Curve Parameters
| Parameter | Value |
|---|---|
| Field | Prime field, p = 2^256 - 2^224 + 2^192 + 2^96 - 1 |
| Equation | y² = x³ - 3x + b |
| b | 5AC635D8 AA3A93E7 B3EBBD55 769886BC 651D06B0 CC53B0F6 3BCE3C3E 27D2604B |
| Order | n = FFFFFFFF 00000000 FFFFFFFF FFFFFFFF BCE6FAAD A7179E84 F3B9CAC2 FC632551 |
| Security | 128-bit |
Implementation
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
)
// Generate P-256 key pair
func generateP256Keys() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}
// ECDH key exchange with P-256
func ecdhP256(privateKey *ecdsa.PrivateKey, peerPublicKey *ecdsa.PublicKey) []byte {
// Compute shared secret: privateKey * peerPublicKey
x, _ := privateKey.Curve.ScalarMult(
peerPublicKey.X,
peerPublicKey.Y,
privateKey.D.Bytes(),
)
// Use x-coordinate as shared secret
return x.Bytes()
}
// JWT with ES256 (ECDSA with P-256 and SHA-256)
func signJWT(privateKey *ecdsa.PrivateKey, payload []byte) (string, error) {
// Create JWT header
header := base64.RawURLEncoding.EncodeToString([]byte(
`{"alg":"ES256","typ":"JWT"}`,
))
// Encode payload
encodedPayload := base64.RawURLEncoding.EncodeToString(payload)
// Create signing input
signingInput := header + "." + encodedPayload
hash := sha256.Sum256([]byte(signingInput))
// Sign with ECDSA
r, s, err := ecdsa.Sign(rand.Reader, privateKey, hash[:])
if err != nil {
return "", err
}
// Encode signature
signature := append(r.Bytes(), s.Bytes()...)
encodedSignature := base64.RawURLEncoding.EncodeToString(signature)
return signingInput + "." + encodedSignature, nil
}Hardware Security Module Integration
// HSM-backed P-256 operations
type HSMSigner struct {
keyHandle string
hsm HSMInterface
}
func (h *HSMSigner) Sign(digest []byte) ([]byte, error) {
// Request signature from HSM
return h.hsm.SignP256(h.keyHandle, digest)
}
func (h *HSMSigner) PublicKey() (*ecdsa.PublicKey, error) {
// Retrieve public key from HSM
pubKeyBytes, err := h.hsm.GetPublicKey(h.keyHandle)
if err != nil {
return nil, err
}
// Parse public key
x, y := elliptic.Unmarshal(elliptic.P256(), pubKeyBytes)
return &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: x,
Y: y,
}, nil
}BN256 (Barreto-Naehrig)
The bn256 curve is a pairing-friendly curve used for advanced cryptographic protocols like zk-SNARKs.
Curve Properties
| Property | Value |
|---|---|
| Embedding Degree | k = 12 |
| Field Size | 256 bits |
| Security Level | ~100 bits (due to pairing attacks) |
| Pairing Type | Type-3 (asymmetric) |
| Groups | G1 (on base curve), G2 (on twist), GT (target) |
Implementation
import (
"github.com/luxfi/crypto/bn256"
)
// Pairing-based cryptography example
func pairingExample() {
// Generate random scalars
a, _ := rand.Int(rand.Reader, bn256.Order)
b, _ := rand.Int(rand.Reader, bn256.Order)
// Compute group elements
ga := new(bn256.G1).ScalarBaseMult(a) // a*G1
gb := new(bn256.G2).ScalarBaseMult(b) // b*G2
// Compute pairing
e_ab := bn256.Pair(ga, gb) // e(a*G1, b*G2)
// Verify bilinearity: e(a*G1, b*G2) = e(G1, G2)^(ab)
g1 := new(bn256.G1).ScalarBaseMult(big.NewInt(1))
g2 := new(bn256.G2).ScalarBaseMult(big.NewInt(1))
e_1 := bn256.Pair(g1, g2)
ab := new(big.Int).Mul(a, b)
ab.Mod(ab, bn256.Order)
e_1_ab := new(bn256.GT).ScalarMult(e_1, ab)
fmt.Printf("Pairing verified: %v\n", e_ab.String() == e_1_ab.String())
}
// BLS signature aggregation using bn256
func blsWithBN256() {
// Generate BLS key pair
privateKey, _ := rand.Int(rand.Reader, bn256.Order)
publicKey := new(bn256.G2).ScalarBaseMult(privateKey)
// Sign message
message := []byte("Hello, BLS!")
h := hashToG1(message)
signature := new(bn256.G1).ScalarMult(h, privateKey)
// Verify signature
g2 := new(bn256.G2).ScalarBaseMult(big.NewInt(1))
lhs := bn256.Pair(signature, g2)
rhs := bn256.Pair(h, publicKey)
fmt.Printf("Signature valid: %v\n", lhs.String() == rhs.String())
}
// Hash to curve for BN256 G1
func hashToG1(message []byte) *bn256.G1 {
// Simplified hash-to-curve (not constant-time)
h := sha256.Sum256(message)
scalar := new(big.Int).SetBytes(h[:])
scalar.Mod(scalar, bn256.Order)
return new(bn256.G1).ScalarBaseMult(scalar)
}zk-SNARK Implementation
Basic zk-SNARK using Groth16 on bn256:
// Groth16 proof system components
type Groth16Proof struct {
A *bn256.G1
B *bn256.G2
C *bn256.G1
}
type VerificationKey struct {
AlphaG1 *bn256.G1
BetaG2 *bn256.G2
GammaG2 *bn256.G2
DeltaG2 *bn256.G2
IC []*bn256.G1 // Input commitments
}
func verifyGroth16(vk *VerificationKey, proof *Groth16Proof, publicInputs []*big.Int) bool {
// Compute input commitment
inputCommitment := new(bn256.G1).Set(vk.IC[0])
for i, input := range publicInputs {
term := new(bn256.G1).ScalarMult(vk.IC[i+1], input)
inputCommitment = new(bn256.G1).Add(inputCommitment, term)
}
// Verify pairing equation:
// e(A, B) = e(alpha, beta) * e(IC, gamma) * e(C, delta)
// Left side: e(A, B)
left := bn256.Pair(proof.A, proof.B)
// Right side components
e1 := bn256.Pair(vk.AlphaG1, vk.BetaG2)
e2 := bn256.Pair(inputCommitment, vk.GammaG2)
e3 := bn256.Pair(proof.C, vk.DeltaG2)
// Multiply pairings
right := new(bn256.GT).Set(e1)
right = new(bn256.GT).Add(right, e2)
right = new(bn256.GT).Add(right, e3)
return left.String() == right.String()
}Performance Comparison
Benchmark results on Apple M1:
| Operation | secp256k1 | secp256r1 | bn256 G1 | bn256 Pairing |
|---|---|---|---|---|
| Key Generation | 28 μs | 15 μs | 25 μs | - |
| Sign | 45 μs | 32 μs | - | - |
| Verify | 88 μs | 95 μs | - | - |
| Scalar Mult | 42 μs | 38 μs | 180 μs | - |
| Pairing | - | - | - | 2.1 ms |
Key Management
Hierarchical Deterministic (HD) Wallets
Implement BIP-32 HD wallets:
import (
"github.com/luxfi/crypto/bip32"
"github.com/luxfi/crypto/bip39"
)
// HD wallet implementation
type HDWallet struct {
masterKey *bip32.Key
mnemonic string
}
func NewHDWallet(mnemonic string, passphrase string) (*HDWallet, error) {
// Generate seed from mnemonic
seed := bip39.NewSeed(mnemonic, passphrase)
// Generate master key
masterKey, err := bip32.NewMasterKey(seed)
if err != nil {
return nil, err
}
return &HDWallet{
masterKey: masterKey,
mnemonic: mnemonic,
}, nil
}
// Derive child keys using BIP-44 path
func (w *HDWallet) DeriveKey(coinType, account, change, index uint32) (*ecdsa.PrivateKey, error) {
// m/44'/coinType'/account'/change/index
path := []uint32{
44 | 0x80000000, // purpose (hardened)
coinType | 0x80000000, // coin type (hardened)
account | 0x80000000, // account (hardened)
change, // change
index, // address index
}
key := w.masterKey
for _, childNum := range path {
var err error
key, err = key.NewChildKey(childNum)
if err != nil {
return nil, err
}
}
// Convert to ECDSA private key
privateKey := &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: secp256k1.S256(),
},
D: new(big.Int).SetBytes(key.Key),
}
privateKey.PublicKey.X, privateKey.PublicKey.Y = privateKey.Curve.ScalarBaseMult(key.Key)
return privateKey, nil
}Security Best Practices
Secure Key Generation
// Always use crypto/rand for key generation
func generateSecureKey() (*ecdsa.PrivateKey, error) {
// Use crypto/rand (never math/rand!)
return ecdsa.GenerateKey(secp256k1.S256(), rand.Reader)
}
// Validate keys before use
func validateKey(privateKey *ecdsa.PrivateKey) error {
// Check key is not zero
if privateKey.D.Sign() == 0 {
return errors.New("private key is zero")
}
// Check key is within curve order
if privateKey.D.Cmp(privateKey.Curve.Params().N) >= 0 {
return errors.New("private key >= curve order")
}
// Verify public key is on curve
if !privateKey.Curve.IsOnCurve(privateKey.PublicKey.X, privateKey.PublicKey.Y) {
return errors.New("public key not on curve")
}
return nil
}Nonce Generation
// RFC 6979 deterministic nonce generation
func deterministicNonce(privateKey *big.Int, message []byte) *big.Int {
// Implement RFC 6979 for deterministic k
// This prevents nonce reuse attacks
h := sha256.Sum256(message)
// HMAC-based deterministic nonce
// (simplified - use proper RFC 6979 implementation)
hmac := hmac.New(sha256.New, privateKey.Bytes())
hmac.Write(h[:])
k := new(big.Int).SetBytes(hmac.Sum(nil))
// Ensure k is in range [1, n-1]
n := secp256k1.S256().Params().N
k.Mod(k, new(big.Int).Sub(n, big.NewInt(1)))
k.Add(k, big.NewInt(1))
return k
}Side-Channel Protection
// Constant-time operations
import "crypto/subtle"
func constantTimeCompare(a, b []byte) bool {
return subtle.ConstantTimeCompare(a, b) == 1
}
// Blinding for signing operations
func blindedSign(privateKey *ecdsa.PrivateKey, hash []byte) (r, s *big.Int, err error) {
curve := privateKey.Curve
n := curve.Params().N
// Generate blinding factor
blind, err := rand.Int(rand.Reader, n)
if err != nil {
return nil, nil, err
}
// Blind the private key
blindedKey := new(big.Int).Mul(privateKey.D, blind)
blindedKey.Mod(blindedKey, n)
// Sign with blinded key
// ... signing operation ...
// Unblind the signature
blindInv := new(big.Int).ModInverse(blind, n)
s = new(big.Int).Mul(s, blindInv)
s.Mod(s, n)
return r, s, nil
}Testing
Comprehensive test suite:
func TestEllipticCurves(t *testing.T) {
curves := []struct {
name string
curve elliptic.Curve
}{
{"secp256k1", secp256k1.S256()},
{"P-256", elliptic.P256()},
}
for _, c := range curves {
t.Run(c.name, func(t *testing.T) {
// Generate key pair
privateKey, err := ecdsa.GenerateKey(c.curve, rand.Reader)
require.NoError(t, err)
// Test signing and verification
message := []byte("test message")
hash := sha256.Sum256(message)
r, s, err := ecdsa.Sign(rand.Reader, privateKey, hash[:])
require.NoError(t, err)
valid := ecdsa.Verify(&privateKey.PublicKey, hash[:], r, s)
require.True(t, valid)
// Test invalid signature
s.Add(s, big.NewInt(1))
valid = ecdsa.Verify(&privateKey.PublicKey, hash[:], r, s)
require.False(t, valid)
})
}
}
func TestPairing(t *testing.T) {
// Test bilinearity
a, _ := rand.Int(rand.Reader, bn256.Order)
b, _ := rand.Int(rand.Reader, bn256.Order)
ga := new(bn256.G1).ScalarBaseMult(a)
gb := new(bn256.G2).ScalarBaseMult(b)
// e(a*G1, b*G2) should equal e(G1, G2)^(ab)
lhs := bn256.Pair(ga, gb)
g1 := new(bn256.G1).ScalarBaseMult(big.NewInt(1))
g2 := new(bn256.G2).ScalarBaseMult(big.NewInt(1))
base := bn256.Pair(g1, g2)
ab := new(big.Int).Mul(a, b)
ab.Mod(ab, bn256.Order)
rhs := new(bn256.GT).ScalarMult(base, ab)
require.Equal(t, lhs.String(), rhs.String())
}