|
| 1 | +package server |
| 2 | + |
| 3 | +import ( |
| 4 | + "crypto/ecdsa" |
| 5 | + "crypto/elliptic" |
| 6 | + "encoding/hex" |
| 7 | + "errors" |
| 8 | + "fmt" |
| 9 | + "math/big" |
| 10 | + "strings" |
| 11 | + |
| 12 | + "golang.org/x/crypto/sha3" |
| 13 | +) |
| 14 | + |
| 15 | +// secp256k1 curve parameters |
| 16 | +var secp256k1 = &elliptic.CurveParams{ |
| 17 | + P: fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"), |
| 18 | + N: fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"), |
| 19 | + B: big.NewInt(7), |
| 20 | + Gx: fromHex("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"), |
| 21 | + Gy: fromHex("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"), |
| 22 | + BitSize: 256, |
| 23 | + Name: "secp256k1", |
| 24 | +} |
| 25 | + |
| 26 | +func fromHex(s string) *big.Int { |
| 27 | + n, _ := new(big.Int).SetString(s, 16) |
| 28 | + return n |
| 29 | +} |
| 30 | + |
| 31 | +// keccak256 computes the Keccak-256 hash. |
| 32 | +func keccak256(data []byte) []byte { |
| 33 | + h := sha3.NewLegacyKeccak256() |
| 34 | + h.Write(data) |
| 35 | + return h.Sum(nil) |
| 36 | +} |
| 37 | + |
| 38 | +// RecoverAddress recovers an Ethereum address from an EIP-191 personal_sign signature. |
| 39 | +// message: the original message text (without prefix) |
| 40 | +// signature: hex-encoded 65-byte signature (r || s || v) |
| 41 | +func RecoverAddress(message string, signature string) (string, error) { |
| 42 | + sig, err := hex.DecodeString(strings.TrimPrefix(signature, "0x")) |
| 43 | + if err != nil { |
| 44 | + return "", fmt.Errorf("invalid signature hex: %w", err) |
| 45 | + } |
| 46 | + if len(sig) != 65 { |
| 47 | + return "", fmt.Errorf("signature must be 65 bytes, got %d", len(sig)) |
| 48 | + } |
| 49 | + |
| 50 | + // EIP-191 personal_sign prefix |
| 51 | + prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message)) |
| 52 | + hash := keccak256(append([]byte(prefix), []byte(message)...)) |
| 53 | + |
| 54 | + // Extract r, s, v from signature |
| 55 | + r := new(big.Int).SetBytes(sig[:32]) |
| 56 | + s := new(big.Int).SetBytes(sig[32:64]) |
| 57 | + v := sig[64] |
| 58 | + |
| 59 | + // Normalize v (27/28 -> 0/1) |
| 60 | + if v >= 27 { |
| 61 | + v -= 27 |
| 62 | + } |
| 63 | + if v > 1 { |
| 64 | + return "", errors.New("invalid signature recovery id") |
| 65 | + } |
| 66 | + |
| 67 | + // Recover public key |
| 68 | + pubKey, err := recoverPubKey(hash, r, s, v) |
| 69 | + if err != nil { |
| 70 | + return "", fmt.Errorf("public key recovery failed: %w", err) |
| 71 | + } |
| 72 | + |
| 73 | + // Derive Ethereum address from public key |
| 74 | + pubBytes := elliptic.Marshal(secp256k1, pubKey.X, pubKey.Y) |
| 75 | + addr := keccak256(pubBytes[1:]) // skip 0x04 prefix |
| 76 | + return "0x" + hex.EncodeToString(addr[12:]), nil |
| 77 | +} |
| 78 | + |
| 79 | +// recoverPubKey recovers the ECDSA public key from a signature. |
| 80 | +func recoverPubKey(hash []byte, r, s *big.Int, v byte) (*ecdsa.PublicKey, error) { |
| 81 | + // Calculate R point on the curve |
| 82 | + rx := new(big.Int).Set(r) |
| 83 | + if v == 1 { |
| 84 | + rx.Add(rx, secp256k1.N) |
| 85 | + } |
| 86 | + |
| 87 | + // Calculate y from x on secp256k1: y^2 = x^3 + 7 |
| 88 | + ry := decompressPoint(rx, v%2 == 1) |
| 89 | + if ry == nil { |
| 90 | + return nil, errors.New("invalid signature: point not on curve") |
| 91 | + } |
| 92 | + |
| 93 | + // R = (rx, ry) |
| 94 | + // e = hash as big.Int |
| 95 | + e := new(big.Int).SetBytes(hash) |
| 96 | + |
| 97 | + // Recover public key: Q = r^-1 * (s*R - e*G) |
| 98 | + rInv := new(big.Int).ModInverse(r, secp256k1.N) |
| 99 | + if rInv == nil { |
| 100 | + return nil, errors.New("invalid signature: r has no inverse") |
| 101 | + } |
| 102 | + |
| 103 | + // s*R |
| 104 | + sRx, sRy := secp256k1.ScalarMult(rx, ry, s.Bytes()) |
| 105 | + |
| 106 | + // e*G |
| 107 | + eGx, eGy := secp256k1.ScalarBaseMult(e.Bytes()) |
| 108 | + |
| 109 | + // s*R - e*G |
| 110 | + eGy.Neg(eGy) |
| 111 | + eGy.Mod(eGy, secp256k1.P) |
| 112 | + sumX, sumY := secp256k1.Add(sRx, sRy, eGx, eGy) |
| 113 | + |
| 114 | + // Q = r^-1 * (s*R - e*G) |
| 115 | + qx, qy := secp256k1.ScalarMult(sumX, sumY, rInv.Bytes()) |
| 116 | + |
| 117 | + return &ecdsa.PublicKey{Curve: secp256k1, X: qx, Y: qy}, nil |
| 118 | +} |
| 119 | + |
| 120 | +// decompressPoint finds the y coordinate for a given x on secp256k1. |
| 121 | +func decompressPoint(x *big.Int, odd bool) *big.Int { |
| 122 | + // y^2 = x^3 + 7 (mod p) |
| 123 | + x3 := new(big.Int).Mul(x, x) |
| 124 | + x3.Mul(x3, x) |
| 125 | + x3.Mod(x3, secp256k1.P) |
| 126 | + |
| 127 | + y2 := new(big.Int).Add(x3, big.NewInt(7)) |
| 128 | + y2.Mod(y2, secp256k1.P) |
| 129 | + |
| 130 | + // sqrt via Tonelli-Shanks (p ≡ 3 mod 4 for secp256k1) |
| 131 | + exp := new(big.Int).Add(secp256k1.P, big.NewInt(1)) |
| 132 | + exp.Rsh(exp, 2) |
| 133 | + y := new(big.Int).Exp(y2, exp, secp256k1.P) |
| 134 | + |
| 135 | + // Verify |
| 136 | + check := new(big.Int).Mul(y, y) |
| 137 | + check.Mod(check, secp256k1.P) |
| 138 | + if check.Cmp(y2) != 0 { |
| 139 | + return nil |
| 140 | + } |
| 141 | + |
| 142 | + // Adjust parity |
| 143 | + if odd != (y.Bit(0) == 1) { |
| 144 | + y.Sub(secp256k1.P, y) |
| 145 | + } |
| 146 | + |
| 147 | + return y |
| 148 | +} |
0 commit comments