现充|junyu33

calculate weil-pairing from 0 to 1

为了更好理解 weil-pairing 以及 miller 算法,这里使用 python 从 0 开始实现了一遍。

用到的依赖只有 pow 函数,math.inf 和深拷贝函数 copy.deepcopy,且使用了本人并不顺手的 OOP 编程范式。

虽然写了之后对 weil-pairing 和 OOP 的理解同时加深了(

Fp 开始

我们在高中竞赛时学过 Fp 的运算。它的加减乘运算都很简单,只需在普通运算(这里指代码中的加减乘运算)的基础上 modp 即可。而对于除法,我们学过至少两种方法(费马小定理和 exgcd)求一个元素在 Fp 上的逆元,从而可以化除为乘。

然而今天没有必要再讲一遍 exgcd,因为先前有一篇 post 已经详细介绍了其原理,这里我选择直接使用 python 的语法糖:

pow(x, -1, p)

来求 x1modp.

于是对于分数 ab,我们也可以用 a×b1modp 来表达,Fp 上的运算我们就处理完了。

引入椭圆曲线

这里引出本 post 的主角,一个定义在 F631 上的椭圆曲线 E

y2=x3+30x+34

这里我们使用一个类 EpCurve,来完成对它的抽象。目前,这个类已经可以放入求逆元除法两个方法,为了体现这个曲线的特性,我们可以加一个判断点 (x,y) 是否在这个曲线的方法,于是我们可以写出完整的实现:

class EpCurve:
    def __init__(self, a, b, p) -> None:
        self.A = a
        self.B = b
        self.mod = p
    
    def inverse(self, x:int) -> int:
        return pow(x, -1, self.mod) 

    def frac(self, x:int, y:int) -> int:
        return x * self.inverse(y) % self.mod
    
    def isOnCurve(self, x, y) -> bool:
        return y*y % self.mod == (x*x*x + self.A*x + self.B) % self.mod

以及对应的实例 curve = EpCurve(30, 34, 631)

然后我们可以在 EpCurve 的基础上,抽象椭圆曲线上的点 EpPoint

class EpPoint(EpCurve):
    def __init__(self, x, y, curve) -> None: 
        super().__init__(curve.A, curve.B, curve.mod)
        if not self.isOnCurve(x, y):
            raise ValueError(f"Point ({x}, {y}) is not on the curve")
        self.x = x % self.mod
        self.y = y % self.mod
        self.O = False
        self.curve = curve

    def __str__(self) -> str:
        if self.O:
            return 'O'
        return f"({self.x}, {self.y})"

这里 self.x self.y 就是点对应的坐标 (x,y),而 self.O 对应了它是不是无穷远点 O__str__ 方法就是 python 内置的一个魔术方法,用于在 print 这个实例时调用这个函数输出对应的字符串。

点的运算

在大二下期的应用密码学课上,我们系统学习了椭圆曲线上点的运算,知道了如何进行点加运算与倍点运算:

对于两个点P=(x1,y1),Q=(x2,y2),我们定义:

λ={y2y1x2x1modp(PQ)3x12+a2y1modpelse

于是我们得到 P+Q 的坐标 (x3,y3) 满足:

{x3=λ2x1x2modpy3=λ(x1x3)y1modp

在几何意义上 P+Q 等同于连接 P,Q 的直线与 E 的第三个交点沿 x 轴的对称点。而 P,也就是 P 沿 x 轴的对称点。

对于无穷远点 OOE 上所有点构成的循环群中的单位元,因此 O+P=P+O=P,写代码的时候稍微特判一下即可:

    def calcLam(self, other) -> int:
        x1, x2, y1, y2 = self.x, other.x, self.y, other.y
        
        if x2 == x1 and y2 == y1:
            # P == Q
            if y1 == 0:
                return math.inf
            return self.frac(3*x1*x1 + self.A, 2*y1) # s = lambda
        else:
            # P != Q
            if x2 == x1:
                return math.inf
            return self.frac(y2 - y1, x2 - x1)

    def add(self, other):        
        if self.O:  # is the point at infinity
            # O + P = P
            return EpPoint(other.x, other.y, self.curve)
        if other.O:
            # P + O = P
            return EpPoint(self.x, self.y, self.curve)
        
        s = self.calcLam(other)
        if s == math.inf:
            res = copy.deepcopy(self)
            res.O = True 
            return res
        
        x1, x2, y1, y2 = self.x, other.x, self.y, other.y
        x3 = (s*s - x1 - x2) % self.mod
        y3 = (s * (x1 - x3) - y1) % self.mod
        
        return EpPoint(x3, y3, self.curve)

对于点的乘法,当然可以使用类似于快速幂的算法解决。但只不过之后的内容跟点乘运算关系不大,这里就直接使用普通的累加解决问题,用于调试的正确性检查:

    def mul(self, x): # check only, no performance, calc x*P
        res = self
        for i in range(x - 1):
            res = res.add(self)
        return res

阶的计算

原理也很简单,对一个点进行累加,如果变成无穷远点就代表这个点的阶(也就是累加次数)找到了。

    def calcOrder(self) -> int:
        back = self; cnt = 1

        while True:
            cnt += 1
            back = back.add(self)
            if back.O == 1:
                return cnt

应用密码学教材的内容就到此为止了。

miller 算法

我们首先简要介绍一下有理函数和除子 div 的概念:

对于任意单变量有理函数:

f(X)=a0+a1X+a2X2++anXnb0+b1X+b2X2++bmXm

根据代数基本定理,我们总可以在复平面上将其因式分解为:

f(X)=a(Xα1)e1(Xα2)e2(Xαr)erb(Xβ1)d1(Xβ2)d2(Xβs)ds

这里 αi,βjr+s 个数互不相同。

为了简便,我们可以使用除子将式子简化:

div(f)=e1[α1]+e2[α2]++er[αr]d1[β1]d2[β2]dr[βr]

这里的αi被称为 zeros(零点),βi 被成为 poles(极点),而前面的系数ei,di则被称为 multiplicity(重数)。

当然,这里的 X 大写就意味着 f 的对象不一定只能是一个数,X 的本身也可以由多变量的函数构成,因此 f 也变成了多变量函数。进一步地,如果f=f(x,y) 中的 (x,y) 由椭圆曲线 E 所约束,那么 E 中所对应的点有的可以让分子为零,有的可以让分母为零,因此这些点也可以被分别称为零点和极点。

e.g. 我们令E:y2=(xα1)(xα2)(xα3), PE, f(P)=f(x,y)=y.

那么 P1,P2,P3=(α1,0),(α2,0),(α3,0) 肯定在 E 上。因此:

div(f)=[P1]+[P2]+[P3]3[O]

这里[P1]+[P2]+[P3] 很好理解,因为此时 f(x,y)=y=0。虽然f(x,y) 并没有分母,但极点O的重数是可以通过射影变换与无穷小分析证明是3的,这里篇幅原因不做展开。

经过上述例子我们可以猜想 Ediv(f) 的一些性质:

  1. div(f)=div(g),当且仅当存在常数cf=cg.
  2. 展开式系数(度数)之和为0.
  3. 如果对这几个点做点加运算,结果为 O.
  4. 特别地,如果不存在零点和极点,那么 f 为常数。

前者被称为除子的唯一性定理,后三者被称为主除子定理。结论与有理函数f的选取无关。


基础知识铺垫完成,现在引入 miller 算法中位于核心地位的 line function gP,Q

gP,Q={yyPλ(xxP)x+xP+xQλ2(λ)xxPelse

显然,gP,Q 也是一个有理函数,我们很容易发现P,Q是两个零点,代入一下点加公式可以知道 P+Q 使得分母为零且分子不为零,是极点并且重数为1。因此根据主除子定理:

div(gP,Q)=[P]+[Q][P+Q][O]

回到代码实现,由于这里gP,Q可以抽象为 g(x,y),并且之后计算gP,Q要依赖一个具体的点S,因此这里可以直接将P,Q,S都作为函数的参数:

    def lineFunc(self, other, S) -> int: # P, Q, S. S is the f_p(S)
        xp, xq, yp, yq = self.x, other.x, self.y, other.y

        if self.calcLam(other) == math.inf:
            return S.x - xp
        lam = self.calcLam(other)
        return self.frac(S.y - yp - lam*(S.x - xp), S.x + xp + xq - lam*lam)

然后来到了 miller 算法本体:

设正整数 m 的二进制表示为 m=bn1bn2...b0,bi{0,1},bn1=1,以下算法可以生成一个有理函数 fP,使得 div(fP)=m[P][mP](m1)[O]

T = P, f = 1
for i from (n-2) to 0:
    f = f^2 * g_{T,T}
    T = 2T
    if b[i] == 1:
        f = f * g_{T,P}
        T = T + P
return f

使用归纳法证明如下:

首先 m1=bn1 时,算法返回 1T=P1是常数,既没有零点也没有极点。将m=1 代入 div(fP),所有的项都消掉了,因此 m1=bn1 成立。

mi=bn1bn2...bni。有 T=miPdiv(fP)=mi[P][miP](mi1)[O] 成立。我们需要证明 mi+1=bn1bn2...bni1 时的情况成立:

假设 bni1=0,则不走 if 分支,我们实际的运算为 f=f2gmiP,miP,miP=2miP。则此时新的 div(f)

=2(mi[P][miP](mi1)[O])+([miP]+[miP][2miP][O])=mi+1[P]2[miP]2(mi1)[O]+2[miP][mi+1]P[O]=mi+1[P][mi+1]P(mi+11)[O]

结果正确,并且此时 T=2T=2miP=mi+1P,也符合归纳要求,因此归纳在 bni1=0 分支下成立。同理也可证得 bni1=1 分支下成立。

综上所述,该算法可以生成有理函数 fP, 使得 div(fP)=m[P][mP](m1)[O]。Q.E.D

特别的,如果点 PE[m](即 P 在由椭圆曲线上的点构成的阶为 m 的群上),那么 div(fP)=m[P]m[O],符合之后要提到的 weil pairing 的条件。

回到代码实现,由于 miller 算法是基于 line function gP,Q 的,并且需要一个辅助点 S 进行具体的数值计算,因此编码思路与 line function 一致:

    def miller(self, S) -> int: # P, S, calc f_P(S)
        if self.O:
            return 1
        T = self; f = 1
        n = bin(self.calcOrder())[3:] # 0b1 01001..101

        for bit in n:
            f = f * f * T.lineFunc(T, S) % self.mod
            T = T.add(T)
            if bit == '1':
                f = f * T.lineFunc(self, S) % self.mod
                T = T.add(self) 
        return f

weil pairing

我们设有理函数 fP,fQ 满足 div(fP)=m[P]m[O],div(fQ)=m[Q]m[O]。假设P,QE[m],且 S 不在P,Q 张成的群里,则定义 weil pairing:

em(P,Q)=fP(Q+S)fP(S)/fQ(PS)fQ(S)

weil pairing 的值除了与有理函数 fP,fQ 以及 S 的选取无关外,还有以下几种性质:

由于我们已经使用 miller 算法计算出了 fP(S),因此我们可以直接通过 miller 类实现 weil 类:

    def weil(self, Q, S) -> int: # P, Q, S
        negS = EpPoint(S.x, -S.y, self.curve)    # -S

        res1 = self.frac(self.miller(Q.add(S)), self.miller(S))
        res2 = self.frac(Q.miller(self.add(negS)), Q.miller(negS))

        return self.frac(res1, res2)

例如取:

P = EpPoint(36, 60, curve)    # Order 5
Q = EpPoint(121, 387, curve)  # Order 5
S = EpPoint(0, 36, curve)     # Order 130

这里的 PQ “线性无关”,而 S 不在 PQ 的“线性组合”内(即找不到 m,n 使得 S=mP+nQ),因此可以用来进行 weil pairing 的计算。

print('P, Q\'s weil pairing:', P.weil(Q, S))
# P, Q's weil pairing: 242
print(242**5 % 631) # property 1 holds
# 1
print('Q, P\'s weil pairing:', Q.weil(P, S))
# Q, P's weil pairing: 279
print(242*279 % 631) # property 4 holds
# 1

然后我们再举另一个例子:

P3 = P.mul(3)
Q4 = Q.mul(4)
print('P3, Q4\'s weil pairing:', P3.weil(Q4, S))
# P3, Q4's weil pairing: 512
print(242**12 % 631) # property 2 holds
# 512

举例说明性质 3 也很简单:

O = P.mul(5)
print('P, O\'s weil pairing:', P.weil(O, S)) # property 3 holds
# P, O's weil pairing: 1

tate pairing

tate pairing 与 weil pairing 的区别是其椭圆曲线定义在有限域 Fq 上,而后者可以是任意域。我们定义:

τ^(P,Q)=(fP(Q+S)fP(S))(q1)/lFq

其中 l 是质数, PE(Fq)[l]QE(Fq), q1(modl).

满足 weil pairing 中的单位根与双线性这两个性质。

可以发现,tate pairing 的计算量比 weil pairing 少一半,所以在密码学中更受青睐。例如 tate pairing 的变种 ate pairing 在以太坊中被广泛使用。

    def tate(self, Q, S) -> int:
        q = self.mod; l = self.calcOrder()
        if q % l != 1:
            raise ValueError('q and l don\'t suuport q == 1 mod l')

        tau = self.frac(self.miller(Q.add(S)), self.miller(S))
        return pow(tau, self.frac(q-1, l), q)

然后我们验证一下性质:

print('P, Q\'s tate pairing:', P.tate(Q, S))
# P, Q's tate pairing: 279
print('Q, P\'s tate pairing:', Q.tate(P, S))
# Q, P's tate pairing: 228

显然279228 都是五次单位根,满足单位根性质;但 279×2281,因此交错性不满足。

然后看一下双线性:

P3 = P.mul(3)
Q2 = Q.mul(2)

print('P, Q\'s tate pairing:', P.tate(Q, S))
# P, Q's tate pairing: 279
print('3P, 2Q\'s tate pairing:', P3.tate(Q2, S))
# 3P, 2Q's tate pairing: 279

因为 2796=2796%5=279,因此满足双线性性质。

完整代码

# y^2 = x^3 + 30x + 34
import copy, math

class EpCurve:
    def __init__(self, a, b, p) -> None:
        self.A = a
        self.B = b
        self.mod = p
    
    def inverse(self, x:int) -> int:
        return pow(x, -1, self.mod) 

    def frac(self, x:int, y:int) -> int:
        return x * self.inverse(y) % self.mod
    
    def isOnCurve(self, x, y) -> bool:
        return y*y % self.mod == (x*x*x + self.A*x + self.B) % self.mod

class EpPoint(EpCurve):
    def __init__(self, x, y, curve) -> None: 
        super().__init__(curve.A, curve.B, curve.mod)
        if not self.isOnCurve(x, y):
            raise ValueError(f"Point ({x}, {y}) is not on the curve")
        self.x = x % self.mod
        self.y = y % self.mod
        self.O = False
        self.curve = curve

    def __str__(self) -> str:
        if self.O:
            return 'O'
        return f"({self.x}, {self.y})"

    def calcLam(self, other) -> int:
        x1, x2, y1, y2 = self.x, other.x, self.y, other.y
        
        if x2 == x1 and y2 == y1:
            if y1 == 0:
                return math.inf
            return self.frac(3*x1*x1 + self.A, 2*y1) # s = lambda
        else:
            if x2 == x1:
                return math.inf
            return self.frac(y2 - y1, x2 - x1)

    def add(self, other):        
        if self.O:
            return EpPoint(other.x, other.y, self.curve)
        if other.O:
            return EpPoint(self.x, self.y, self.curve)
        
        s = self.calcLam(other)
        if s == math.inf:
            res = copy.deepcopy(self)
            res.O = True
            return res
        
        x1, x2, y1, y2 = self.x, other.x, self.y, other.y
        x3 = (s*s - x1 - x2) % self.mod
        y3 = (s * (x1 - x3) - y1) % self.mod
        
        return EpPoint(x3, y3, self.curve)

    def mul(self, x): # check only, no performance
        res = self
        for i in range(x - 1):
            res = res.add(self)
        return res

    def calcOrder(self) -> int:
        back = self; cnt = 1

        while True:
            cnt += 1
            back = back.add(self)
            if back.O == 1:
                return cnt

    def lineFunc(self, other, S) -> int: # S is the f_p(S)
        xp, xq, yp, yq = self.x, other.x, self.y, other.y

        if self.calcLam(other) == math.inf:
            return S.x - xp
        lam = self.calcLam(other)
        return self.frac(S.y - yp - lam*(S.x - xp), S.x + xp + xq - lam*lam)
    
    def miller(self, S) -> int: # P, S
        if self.O:
            return 1
        T = self; f = 1
        n = bin(self.calcOrder())[3:]

        for bit in n:
            f = f * f * T.lineFunc(T, S) % self.mod
            T = T.add(T)
            if bit == '1':
                f = f * T.lineFunc(self, S) % self.mod
                T = T.add(self) 
        return f
    
    def weil(self, Q, S) -> int: # P, Q, S
        negS = EpPoint(S.x, -S.y, self.curve)    # -S

        res1 = self.frac(self.miller(Q.add(S)), self.miller(S))
        res2 = self.frac(Q.miller(self.add(negS)), Q.miller(negS))

        return self.frac(res1, res2)
    
    def tate(self, Q, S) -> int:
        q = self.mod; l = self.calcOrder()
        if q % l != 1:
            raise ValueError('q and l don\'t suuport q == 1 mod l')

        tau = self.frac(self.miller(Q.add(S)), self.miller(S))
        return pow(tau, self.frac(q-1, l), q)


curve = EpCurve(30, 34, 631)  
P = EpPoint(36, 60, curve)  # Order 5
Q = EpPoint(121, 387, curve)
S = EpPoint(0, 36, curve)

# print('P + Q =', P.add(Q))
# print('P + Q =', P.add(P))
# print('Q + S =', Q.add(S))
# print('P\'s order:', P.calcOrder())
# print('P\'s miller function related to S:', P.miller(S))
# print('P + Q\'s miller function related to S:', P.miller(Q.add(S)))
# print('P, Q\'s weil pairing:', P.weil(Q, S))

P3 = P.mul(3)
Q2 = Q.mul(2)
O = P.mul(5)

print('P, Q\'s tate pairing:', P.tate(Q, S)) 
print('3P, 2Q\'s tate pairing:', P3.tate(Q2, S))

参考资料