こんにちは、CTOの森下です。

今回は、BitcoinやEthereumに使われている楕円曲線のsecp256k1を学ぶためにGolangでフルスクラッチで実装してみたいと思います。

楕円曲線

楕円曲線とは、

で定義された双方有理同値な曲線のことです。 楕円曲線についての説明は非常に複雑なのでここでは省略しますが、 楕円曲線暗号はこの楕円曲線の特殊な性質と離散対数問題の困難性を利用しています。

secp256k1

secp256k1とはStandards for Efficient Cryptography[1]で標準化されている楕円曲線でBitcoinやEthereumを始めとする多くのブロックチェーンの署名アルゴリズムに採用されています。 楕円曲線には主に、ビットサイズ、法とする素数p, 曲線の変数 a,b, ベースポイント G, ベースポイントの位数 n といったパラメータがあります。このパラメータがSECで標準化されているものになります。secp256k1のパラメータを以下に示します。

パラメータ

ビットサイズ

bitSize = 256

法とする素数

p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F

曲線

a = 0
b = 7
secp256k1

curve

ベースポイント

Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
// 圧縮形式
G =  0x0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
// 非圧縮形式
G =  0x0479BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8

ベースポイントの位数

n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

secp256k1の実装

今回はこのsecp256k1をGolangで実装します。Golangには標準パッケージに楕円曲線のパッケージ[2]である elliptic が存在しており、以下のようなCurveインターフェースが定義されています。 なので、このインターフェースを満たす構造体を作成していきます。

パラメータの定義

// A Curve represents a short-form Weierstrass curve with a=-3.
// See https://www.hyperelliptic.org/EFD/g1p/auto-shortw.html
type Curve interface {
	// Params returns the parameters for the curve.
	Params() *CurveParams
	// IsOnCurve reports whether the given (x,y) lies on the curve.
	IsOnCurve(x, y *big.Int) bool
	// Add returns the sum of (x1,y1) and (x2,y2)
	Add(x1, y1, x2, y2 *big.Int) (x, y *big.Int)
	// Double returns 2*(x,y)
	Double(x1, y1 *big.Int) (x, y *big.Int)
	// ScalarMult returns k*(Bx,By) where k is a number in big-endian form.
	ScalarMult(x1, y1 *big.Int, k []byte) (x, y *big.Int)
	// ScalarBaseMult returns k*G, where G is the base point of the group
	// and k is an integer in big-endian form.
	ScalarBaseMult(k []byte) (x, y *big.Int)
}

まずは、楕円曲線のパラメータを持つ構造体のCurveParamsを定義します。これは、elliptic パッケージで定義されているものと全く同じものです。

type CurveParams struct {
	P       *big.Int 
	N       *big.Int 
	B       *big.Int 
	Gx, Gy  *big.Int 
	BitSize int      
	Name    string
}

次に、secp256k1を CurveParams 型で定義し、それぞれのパラメータも定義します。

var (
	secp256k1 *CurveParams
	p, _    = new(big.Int).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16)
	n, _    = new(big.Int).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)
	b, _    = new(big.Int).SetString("0000000000000000000000000000000000000000000000000000000000000007", 16)
	gx, _   = new(big.Int).SetString("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 16)
	gy, _   = new(big.Int).SetString("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 16)
	bitSize = 256
	name    = "secp256k1"
)

これでパラメータの定義できました。 ここからはこのインターフェースを満たすメソッドを実装していきます。

Params()

まずは、Params() ですね。 Params()elliptic.CurveParamsのポインタを返すだけのメソッドなので非常にシンプルです。

func (curve *CurveParams) Params() *elliptic.CurveParams {
	return &elliptic.CurveParams{
		P:       p,
		N:       n,
		B:       b,
		Gx:      gx,
		Gy:      gy,
		BitSize: bitSize,
		Name:    name,
	}
}

IsOnCurve()

次は、 IsOnCurve() です。これは与えられた点が楕円曲線上にあるかどうかを返すメソッドです。 楕円曲線の公式に当てはめると

がゼロかどうかを調べれば良いわけですね。

func (curve *CurveParams) IsOnCurve(x, y *big.Int) bool {
	y2 := new(big.Int).Exp(y, big.NewInt(2), nil)
	x3 := new(big.Int).Exp(x, big.NewInt(3), nil)
	ans := new(big.Int).Mod(y2.Sub(y2, x3.Add(x3, curve.B)), curve.P)

	return ans.Cmp(big.NewInt(0)) == 0
}

Add()

Add() 与えられた2点の足し算を行う関数です。楕円曲線暗号には必要ないですが、インターフェースなので一応実装します。 Addの公式はパッケージのリファレンスにあったこのヤコビアン計算の公式[3]を使います。

Z1Z1 = Z1^2
Z2Z2 = Z2^2
U1 = X1*Z2Z2
U2 = X2*Z1Z1
S1 = Y1*Z2*Z2Z2
S2 = Y2*Z1*Z1Z1
H = U2-U1
I = (2*H)^2
J = H*I
r = 2*(S2-S1)
V = U1*I
X3 = r^2-J-2*V
Y3 = r*(V-X3)-2*S1*J
Z3 = ((Z1+Z2)^2-Z1Z1-Z2Z2)*H

Golangには行列の計算を行うパッケージなどはないので公式に従ってBig.Intでゴリゴリ実装しています。

func (curve *CurveParams) addJacobian(x1, y1, z1, x2, y2, z2 *big.Int) (*big.Int, *big.Int, *big.Int) {
	x3, y3, z3 := new(big.Int), new(big.Int), new(big.Int)
	if z1.Sign() == 0 {
		x3.Set(x2)
		y3.Set(y2)
		z3.Set(z2)
		return x3, y3, z3
	}
	if z2.Sign() == 0 {
		x3.Set(x1)
		y3.Set(y1)
		z3.Set(z1)
		return x3, y3, z3
	}

	z1z1 := new(big.Int).Mul(z1, z1)
	z1z1.Mod(z1z1, curve.P)
	z2z2 := new(big.Int).Mul(z2, z2)
	z2z2.Mod(z2z2, curve.P)

	u1 := new(big.Int).Mul(x1, z2z2)
	u1.Mod(u1, curve.P)
	u2 := new(big.Int).Mul(x2, z1z1)
	u2.Mod(u2, curve.P)
	h := new(big.Int).Sub(u2, u1)
	xEqual := h.Sign() == 0
	if h.Sign() == -1 {
		h.Add(h, curve.P)
	}
	i := new(big.Int).Lsh(h, 1)
	i.Mul(i, i)
	j := new(big.Int).Mul(h, i)

	s1 := new(big.Int).Mul(y1, z2)
	s1.Mul(s1, z2z2)
	s1.Mod(s1, curve.P)
	s2 := new(big.Int).Mul(y2, z1)
	s2.Mul(s2, z1z1)
	s2.Mod(s2, curve.P)
	r := new(big.Int).Sub(s2, s1)
	if r.Sign() == -1 {
		r.Add(r, curve.P)
	}
	yEqual := r.Sign() == 0
	if xEqual && yEqual {
		return curve.doubleJacobian(x1, y1, z1)
	}
	r.Lsh(r, 1)
	v := new(big.Int).Mul(u1, i)

	x3.Set(r)
	x3.Mul(x3, x3)
	x3.Sub(x3, j)
	x3.Sub(x3, v)
	x3.Sub(x3, v)
	x3.Mod(x3, curve.P)

	y3.Set(r)
	v.Sub(v, x3)
	y3.Mul(y3, v)
	s1.Mul(s1, j)
	s1.Lsh(s1, 1)
	y3.Sub(y3, s1)
	y3.Mod(y3, curve.P)

	z3.Add(z1, z2)
	z3.Mul(z3, z3)
	z3.Sub(z3, z1z1)
	z3.Sub(z3, z2z2)
	z3.Mul(z3, h)
	z3.Mod(z3, curve.P)

	return x3, y3, z3
}

次は、ヤコビアンからアフィン座標に変換するメソッドです。

func (curve *CurveParams) affineFromJacobian(x, y, z *big.Int) (xOut, yOut *big.Int) {
	if z.Sign() == 0 {
		return new(big.Int), new(big.Int)
	}

	zinv := new(big.Int).ModInverse(z, curve.P)
	zinvsq := new(big.Int).Mul(zinv, zinv)

	xOut = new(big.Int).Mul(x, zinvsq)
	xOut.Mod(xOut, curve.P)

	zinvsq.Mul(zinvsq, zinv)

	yOut = new(big.Int).Mul(y, zinvsq)
	yOut.Mod(yOut, curve.P)

	return
}

そして、アフィン座標 (x,y) に対するヤコビアンの z 値を返すメソッドです。 xy がゼロの場合、無限遠点を表す0を返します。

func zForAffine(x, y *big.Int) *big.Int {
	z := new(big.Int)
	if x.Sign() != 0 || y.Sign() != 0 {
		z.SetInt64(1)
	}

	return z
}

以上の3つの関数を組み合わせることで Add() を以下のように実装できます。

func (curve *CurveParams) Add(x1, y1, x2, y2 *big.Int) (x, y *big.Int) {
	z1 := zForAffine(x1, y1)
	z2 := zForAffine(x2, y2)
	return curve.affineFromJacobian(curve.addJacobian(x1, y1, z1, x2, y2, z2))
}

Double()

Double() は 与えられた点を倍にするメソッドです。これも Add() と同様にヤコビアン計算の公式[4]を使います。

A = X1^2
B = Y1^2
C = B^2
D = 2*((X1+B)^2-A-C)
E = 3*A
F = E^2
X3 = F-2*D
Y3 = E*(D-X3)-8*C
Z3 = 2*Y1*Z1

これをGolangで実装すると以下のようになります。

func (curve *CurveParams) doubleJacobian(x, y, z *big.Int) (*big.Int, *big.Int, *big.Int) {
	a := new(big.Int).Mul(x, x)
	b := new(big.Int).Mul(y, y)
	c := new(big.Int).Mul(b, b)

	d := new(big.Int).Add(x, b)
	d.Mul(d, d)
	d.Sub(d, a)
	d.Sub(d, c)
	d.Mul(d, big.NewInt(2))

	e := new(big.Int).Mul(big.NewInt(3), a)
	f := new(big.Int).Mul(e, e)

	x3 := new(big.Int).Mul(big.NewInt(2), d)
	x3.Sub(f, x3)
	x3.Mod(x3, curve.P)

	y3 := new(big.Int).Sub(d, x3)
	y3.Mul(e, y3)
	y3.Sub(y3, new(big.Int).Mul(big.NewInt(8), c))
	y3.Mod(y3, curve.P)

	z3 := new(big.Int).Mul(y, z)
	z3.Mul(big.NewInt(2), z3)
	z3.Mod(z3, curve.P)

	return x3, y3, z3
}

このメソッドを使って Double() を以下のように定義できます。

func (curve *CurveParams) Double(x1, y1 *big.Int) (x, y *big.Int) {
	z1 := zForAffine(x1, y1)
	return curve.affineFromJacobian(curve.doubleJacobian(x1, y1, z1))
}

ScalarMult()

ScalarMult() は与えられた点を k 倍するメソッドです。 スカラー倍を効率よく求めるには、二進展開法という手法があるのでこれを利用します。 k のビット数を n ビットとすると

このように表現でき、これをGolangで実装してみると以下のようになります。

func (curve *CurveParams) ScalarMult(Bx, By *big.Int, k []byte) (x, y *big.Int) {
	Bz := new(big.Int).SetInt64(1)
	x, y, z := new(big.Int), new(big.Int), new(big.Int)

	for _, byte := range k {
		for bitNum := 0; bitNum < 8; bitNum++ {
            x, y, z = curve.doubleJacobian(x, y, z) // 倍算
			if byte&0x80 == 0x80 { // ビットが1だったら加算
				x, y, z = curve.addJacobian(Bx, By, Bz, x, y, z)
			}
			byte <<= 1
		}
	}

	return curve.affineFromJacobian(x, y, z)
}

ScalarBaseMult()

ScalarBaseMult() は楕円曲線暗号においてメインで使うメソッドで、ベースポイント Gk 倍した値を返します。 これは上で説明した ScalarMult() にベースポイント G を渡すようにするラッパーとして実装すればOKです。

func (curve *CurveParams) ScalarBaseMult(k []byte) (x, y *big.Int) {
	return curve.ScalarMult(curve.Gx, curve.Gy, k)
}

Secp256k1()

最後に init() でsecp256k1を初期化し、それを返す Secp256k1() を定義して完成です。

func init() {
	secp256k1 = &CurveParams{
		P:       p,
		N:       n,
		B:       b,
		Gx:      gx,
		Gy:      gy,
		BitSize: bitSize,
		Name:    name,
	}
}
func Secp256k1() elliptic.Curve {
	return secp256k1
}

コードの全体はここ[5]で公開しています。

使用例

package main

import (
	crypto "github.com/GincoInc/go-crypto"
)

func main() {
	crypto.Secp256k1().Params()
	kx, ky := crypto.Secp256k1().ScalarBaseMult([]byte{0x02})
	// kx: "C6047F9441ED7D6D3045406E95C07CD85C778E4B8CEF3CA7ABAC09B95C709EE5",
	// ky: "1AE168FEA63DC339A3C58419466CEAEEF7F632653266D0E1236431A950CFE52A",
	crypto.Secp256k1().IsOnCurve(kx, ky) // true
}

まとめ

今回はGolangでブロックチェーンの根本技術である楕円曲線暗号に使用する楕円曲線の実装をしました。 標準パッケージのインターフェースで実装することで他の楕円曲線との互換性もあり、使いやすいものになっていると思います。 Golangは便利な計算ライブラリがない反面、このように自分で実装することでより深い知識が得られるので非常に良い言語だと思っています。 また、公式からコードへの実装ができるようになるとホワイトペーパーを理解してコードに落とし込むことが容易になるので、 ブロックチェーンエンジニアになりたい方は是非プリミティブな暗号の実装をおすすめします。

Tip us!

エンジニアチームのブログを書くモチベーションが上がります 💪

address

0xd6d478dCe4585a394834690158cf83581223C08f

参考文献