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

ParameterValue
FieldPrime field, p = 2^256 - 2^32 - 977
Equationy² = x³ + 7
Generator PointG = (0x79BE667E...F9DCBBAC, 0x483ADA77...FED9CB5)
Ordern = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141
Cofactorh = 1
Security128-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

ParameterValue
FieldPrime field, p = 2^256 - 2^224 + 2^192 + 2^96 - 1
Equationy² = x³ - 3x + b
b5AC635D8 AA3A93E7 B3EBBD55 769886BC 651D06B0 CC53B0F6 3BCE3C3E 27D2604B
Ordern = FFFFFFFF 00000000 FFFFFFFF FFFFFFFF BCE6FAAD A7179E84 F3B9CAC2 FC632551
Security128-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

PropertyValue
Embedding Degreek = 12
Field Size256 bits
Security Level~100 bits (due to pairing attacks)
Pairing TypeType-3 (asymmetric)
GroupsG1 (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:

Operationsecp256k1secp256r1bn256 G1bn256 Pairing
Key Generation28 μs15 μs25 μs-
Sign45 μs32 μs--
Verify88 μs95 μs--
Scalar Mult42 μs38 μs180 μ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())
}

References