mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-27 12:24:12 +00:00
- add monorepo Makefile delegating to sub-projects - update CI workflows, GoReleaser, gitignore for new structure - revise README, CONTRIBUTING.md for monorepo overview - bump Go to 1.24
217 lines
6.2 KiB
Go
217 lines
6.2 KiB
Go
package update
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/sha3"
|
|
)
|
|
|
|
// SignerAddress is the Ethereum address authorized to sign OramaOS updates.
|
|
// Updates signed by any other address are rejected.
|
|
const SignerAddress = "0xb5d8a496c8b2412990d7D467E17727fdF5954afC"
|
|
|
|
// verifySignature verifies an EVM personal_sign signature against the expected signer.
|
|
// hashHex is the hex-encoded SHA-256 hash of the update checksum file.
|
|
// signatureHex is the 65-byte hex-encoded EVM signature (r || s || v).
|
|
func verifySignature(hashHex, signatureHex string) error {
|
|
if SignerAddress == "0x0000000000000000000000000000000000000000" {
|
|
return fmt.Errorf("signer address not configured — refusing unsigned update")
|
|
}
|
|
|
|
sigBytes, err := hex.DecodeString(strings.TrimPrefix(signatureHex, "0x"))
|
|
if err != nil {
|
|
return fmt.Errorf("invalid signature hex: %w", err)
|
|
}
|
|
if len(sigBytes) != 65 {
|
|
return fmt.Errorf("invalid signature length: got %d, expected 65", len(sigBytes))
|
|
}
|
|
|
|
// Compute EVM personal_sign message hash
|
|
msgHash := personalSignHash(hashHex)
|
|
|
|
// Split signature into r, s, v
|
|
r := new(big.Int).SetBytes(sigBytes[:32])
|
|
s := new(big.Int).SetBytes(sigBytes[32:64])
|
|
v := sigBytes[64]
|
|
if v >= 27 {
|
|
v -= 27
|
|
}
|
|
if v > 1 {
|
|
return fmt.Errorf("invalid signature recovery id: %d", v)
|
|
}
|
|
|
|
// Recover public key
|
|
pubKey, err := recoverPubkey(msgHash, r, s, v)
|
|
if err != nil {
|
|
return fmt.Errorf("public key recovery failed: %w", err)
|
|
}
|
|
|
|
// Derive Ethereum address
|
|
recovered := pubkeyToAddress(pubKey)
|
|
expected := strings.ToLower(strings.TrimPrefix(SignerAddress, "0x"))
|
|
got := strings.ToLower(strings.TrimPrefix(recovered, "0x"))
|
|
|
|
if got != expected {
|
|
return fmt.Errorf("update signed by 0x%s, expected 0x%s", got, expected)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// personalSignHash computes keccak256("\x19Ethereum Signed Message:\n" + len(msg) + msg).
|
|
func personalSignHash(message string) []byte {
|
|
prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message))
|
|
h := sha3.NewLegacyKeccak256()
|
|
h.Write([]byte(prefix))
|
|
h.Write([]byte(message))
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
// recoverPubkey recovers the ECDSA public key from a secp256k1 signature.
|
|
// Uses the standard EC point recovery algorithm.
|
|
func recoverPubkey(hash []byte, r, s *big.Int, v byte) (*ecdsa.PublicKey, error) {
|
|
// secp256k1 curve parameters
|
|
curve := secp256k1Curve()
|
|
N := curve.Params().N
|
|
P := curve.Params().P
|
|
|
|
if r.Sign() <= 0 || s.Sign() <= 0 {
|
|
return nil, errors.New("invalid signature: r or s is zero")
|
|
}
|
|
if r.Cmp(N) >= 0 || s.Cmp(N) >= 0 {
|
|
return nil, errors.New("invalid signature: r or s >= N")
|
|
}
|
|
|
|
// Step 1: Compute candidate x = r + v*N (v is 0 or 1)
|
|
x := new(big.Int).Set(r)
|
|
if v == 1 {
|
|
x.Add(x, N)
|
|
}
|
|
if x.Cmp(P) >= 0 {
|
|
return nil, errors.New("invalid recovery: x >= P")
|
|
}
|
|
|
|
// Step 2: Recover the y coordinate from x
|
|
Rx, Ry, err := decompressPoint(curve, x)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("point decompression failed: %w", err)
|
|
}
|
|
|
|
// The y parity must match the recovery id
|
|
if Ry.Bit(0) != 0 {
|
|
Ry.Sub(P, Ry) // negate y
|
|
}
|
|
// v determines which y we want: v=0 → even y, v=1 → odd y for the first candidate
|
|
// Actually for EVM: v just selects x=r vs x=r+N. y is chosen to make verification work.
|
|
// We try both y values and verify.
|
|
for _, negateY := range []bool{false, true} {
|
|
testRy := new(big.Int).Set(Ry)
|
|
if negateY {
|
|
testRy.Sub(P, testRy)
|
|
}
|
|
|
|
// Step 3: Compute public key: Q = r^(-1) * (s*R - e*G)
|
|
rInv := new(big.Int).ModInverse(r, N)
|
|
if rInv == nil {
|
|
return nil, errors.New("r has no modular inverse")
|
|
}
|
|
|
|
// s * R
|
|
sRx, sRy := curve.ScalarMult(Rx, testRy, s.Bytes())
|
|
|
|
// e * G (where e = hash interpreted as big.Int)
|
|
e := new(big.Int).SetBytes(hash)
|
|
eGx, eGy := curve.ScalarBaseMult(e.Bytes())
|
|
|
|
// s*R - e*G = s*R + (-e*G)
|
|
negEGy := new(big.Int).Sub(P, eGy)
|
|
qx, qy := curve.Add(sRx, sRy, eGx, negEGy)
|
|
|
|
// Q = r^(-1) * (s*R - e*G)
|
|
qx, qy = curve.ScalarMult(qx, qy, rInv.Bytes())
|
|
|
|
// Verify: the recovered key should produce a valid signature
|
|
pub := &ecdsa.PublicKey{Curve: curve, X: qx, Y: qy}
|
|
if ecdsa.Verify(pub, hash, r, s) {
|
|
return pub, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("could not recover public key from signature")
|
|
}
|
|
|
|
// pubkeyToAddress derives an Ethereum address from a public key.
|
|
// address = keccak256(uncompressed_pubkey_bytes[1:])[12:]
|
|
func pubkeyToAddress(pub *ecdsa.PublicKey) string {
|
|
pubBytes := elliptic.Marshal(pub.Curve, pub.X, pub.Y)
|
|
h := sha3.NewLegacyKeccak256()
|
|
h.Write(pubBytes[1:]) // skip 0x04 prefix
|
|
hash := h.Sum(nil)
|
|
return "0x" + hex.EncodeToString(hash[12:])
|
|
}
|
|
|
|
// decompressPoint recovers the y coordinate from x on the given curve.
|
|
// Solves y² = x³ + 7 (secp256k1: a=0, b=7).
|
|
func decompressPoint(curve elliptic.Curve, x *big.Int) (*big.Int, *big.Int, error) {
|
|
P := curve.Params().P
|
|
|
|
// y² = x³ + b mod P
|
|
x3 := new(big.Int).Mul(x, x)
|
|
x3.Mul(x3, x)
|
|
x3.Mod(x3, P)
|
|
|
|
// b = 7 for secp256k1
|
|
b := big.NewInt(7)
|
|
y2 := new(big.Int).Add(x3, b)
|
|
y2.Mod(y2, P)
|
|
|
|
// y = sqrt(y²) mod P
|
|
// For P ≡ 3 (mod 4), sqrt(a) = a^((P+1)/4) mod P
|
|
// secp256k1's P ≡ 3 (mod 4), so this works.
|
|
exp := new(big.Int).Add(P, big.NewInt(1))
|
|
exp.Rsh(exp, 2) // (P+1)/4
|
|
y := new(big.Int).Exp(y2, exp, P)
|
|
|
|
// Verify
|
|
verify := new(big.Int).Mul(y, y)
|
|
verify.Mod(verify, P)
|
|
if verify.Cmp(y2) != 0 {
|
|
return nil, nil, fmt.Errorf("x=%s is not on the curve", x.Text(16))
|
|
}
|
|
|
|
return x, y, nil
|
|
}
|
|
|
|
// secp256k1Curve returns the secp256k1 elliptic curve used by Ethereum.
|
|
// Go's standard library doesn't include secp256k1, so we define it here.
|
|
func secp256k1Curve() elliptic.Curve {
|
|
return &secp256k1CurveParams
|
|
}
|
|
|
|
var secp256k1CurveParams = secp256k1CurveImpl{
|
|
CurveParams: &elliptic.CurveParams{
|
|
P: hexBigInt("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"),
|
|
N: hexBigInt("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"),
|
|
B: big.NewInt(7),
|
|
Gx: hexBigInt("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"),
|
|
Gy: hexBigInt("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"),
|
|
BitSize: 256,
|
|
Name: "secp256k1",
|
|
},
|
|
}
|
|
|
|
type secp256k1CurveImpl struct {
|
|
*elliptic.CurveParams
|
|
}
|
|
|
|
func hexBigInt(s string) *big.Int {
|
|
n, _ := new(big.Int).SetString(s, 16)
|
|
return n
|
|
}
|