☰
椭圆曲线与群结构:从几何加法到密码学实践
2026/10/2 11:43:58 网站建设 项目流程

1. 从三次方程到椭圆曲线:为什么这东西值得研究

我最早接触椭圆曲线,纯属被名字坑了。当年在资料里看到“椭圆曲线”四个字,第一反应是画一条扁扁的椭圆,结果发现自己根本画不出来,还把方程 y² = x³ + ax + b 丢进绘图软件,出来的曲线是一段一段弯来弯去的波浪。后来才搞清楚,这东西和椭圆的关系非常遥远,只是历史沿革留下的一个名字。但这不妨碍它成为整个数论、代数几何和现代密码学里最炙手可热的结构之一,几乎所有做数学或计算机方向的人都绕不开它。

这篇文章我想聊聊椭圆曲线以及它身上最重要的代数结构:群结构。你可以把它理解成,在一条曲线上定义一种加法,让所有点之间可以进行运算,而这个运算满足交换律、结合律、有单位元、有逆元。一旦这个结构立住,椭圆曲线就从一个静态的几何图形,变成一个可以“做算术”的代数对象。这篇文章适合谁看呢?如果你是数学系低年级学生、自学密码学的程序员、或者对抽象代数和数论感兴趣但一直没找到入口的爱好者,都很合适。我会尽可能把群论里那些绕口的概念用几何和算例讲清楚,还会附上可以直接运行的代码,带你亲手算一遍椭圆曲线上的加法。

我建议你先在纸上写下这个方程:y² = x³ + ax + b。等号右边是x的三次多项式,等号左边是y的平方。这看起来只是多项式,但它的形状非常特殊。为了让这条曲线“好相处”,我们通常要求右边三次多项式没有重根,也就是判别式不发生退化,这一步后续会详细讲。一旦这个条件成立,再补上一个人为规定的“无穷远点”,整条曲线上的点就能构成一个阿贝尔群,也就是交换群。这是椭圆曲线能成为密码学基石的核心原因:一个庞大但封闭的有限集合,加上一种难以逆转的运算,恰好就是很多安全协议需要的“舞台”。

1.1 椭圆曲线的标准形式:为什么长这样

椭圆曲线的标准形式通常写成 y² = x³ + ax + b,这叫做魏尔斯特拉斯(Weierstrass)标准型。你可能疑惑,为什么一定要y的平方配上x的三次方?这背后的原因其实和椭圆积分的历史有关。

早先数学家在研究椭圆周长时,遭遇了形如 ∫ dx / √(x³ + ax + b) 这类积分。这类积分的反函数定义出来的曲线,被叫作椭圆曲线。后来人们发现,研究这类曲线时,直接把它写成代数方程更省事,于是就把目光聚焦到 y² = x³ + ax + b 这条光滑的三次曲线上。你可以把“椭圆”二字理解为它的历史血统,而不是它的几何形状。真正在坐标平面上画出来,它更像一条扭来扭去的三次曲线。

从代数结构的角度看,这个方程之所以“好用”,是因为三次曲线有一个非常棒的几何性质:一条直线如果穿过曲线上两个点,它必然还会穿过第三个点(在复数域上计数,偶尔会重合,需要算重数)。这个二推三的性质,是后面定义群运算的关键素材。如果是二次曲线,这条性质不成立;如果次数超过三,计算又太复杂。三次正好卡在“简单但结构丰富”的甜点上。

1.2 从积分到曲线:名字里藏着的历史包袱

简单说说历史。椭圆曲线的名号,源于椭圆周长计算中出现的椭圆积分。很多教材会一笔带过,但我觉得知道来龙去脉,有助于你想明白为什么曲线和“椭圆”纠缠不清。

椭圆在第一象限的参数方程可以写成 x = a·sinθ,y = b·cosθ,弧长积分算下来会出现 √(1 - k²sin²θ) 这类根式。换元整理后,积分中就出现了三次多项式的平方根。十九世纪阿贝尔和雅可比等数学家在研究这类积分时,发现它们的反函数具有“加法性质”,也就是两个积分值相加,可以通过解一个代数方程得到第三个值。这个加法性质,最终被抽象成椭圆曲线上的群运算。

知道了这段历史,你再看椭圆曲线的群运算,就不会觉得它凭空而降。它其实是古代椭圆积分加法公式的几何化版本。数学家们发现,把积分反函数定义的点放在曲线的坐标系上,它们的“加法”恰好就是几何上三点共线的规律。这样一来,椭圆的物理问题、曲线的代数方程、点的群运算,三个看似无关的东西就串成了一条线。

2. 点集的群结构:如何给曲线上的点做加法

现在进入正题。椭圆曲线的点集能带上一个群结构,这是它最迷人的地方。群这个抽象代数概念,许多人在抽象代数课程里学过,但不知道它怎么落到具体的几何对象上。椭圆曲线是个特别好的例子:它的元素就是曲线上的点,运算则是定义在几何画法之上的“加法”。

先想象一条椭圆曲线:y² = x³ - x,坐标平面上有它的图像。现在,我们给曲线上的任意两个点P和Q定义一个“加法”结果R。几何步骤如下:用一条直线连接P和Q(如果P等于Q,就用曲线在P点的切线),这条直线一定会与曲线产生第三个交点,设为S。接下来,把S关于x轴做对称,对称后的点就是P + Q的结果R。这里对称操作不是为了好看,而是为了确保群运算的单位元能够自然出现,这一点我马上解释。

这套几何定义做了两件关键的事。第一,它把直线的“三交点”性质和群的封闭性绑定。任何两个点加出来的结果仍然在曲线上,绝不会跑出去。第二,它让“无穷远点”成为加法单位元。如果你取P的对称点-P,则P和(-P)的连线是一条竖直直线。从图形上看,这条竖直线与曲线的交点是P、-P,以及无穷远点。规定P + (-P) = 无穷远点,等价于把S(第三个交点)对x轴对称回来,结果就是“无穷远方向的点”。这样一来,逆元的存在性也有了。

2.1 几何视角下的加法规则:连线、取交点、做镜像

我从实际操作的角度,把加法步骤复述一遍。设曲线上有两点P(x₁, y₁)和Q(x₂, y₂)。

  • 如果P ≠ Q,连接P和Q画一条直线。
  • 这条直线与椭圆曲线相交于第三个点S。
  • 将S沿x轴翻折到曲线另一侧的对称点,即得到P + Q。

如果P = Q,也就是要计算“倍点”2P = P + P,就改成过P点作曲线的切线,切线与曲线的另一个交点S翻折后,得到2P。

为什么非要翻折?这里有个特别巧妙的逻辑。如果只取直线与曲线的三个交点,直接定义“第三个交点为和”,那这个运算不满足交换律吗?其实是满足的,因为连线谁先谁后不影响第三个交点。但它带来的单位元问题不好处理。你算P + (-P)时,第三个交点恰好是无穷远点,如果把无穷远点当作结果,那它就天然成为单位元。但这样定义出来的看起来更像“三元运算”。翻折一次之后,运算就变成标准的二元运算,而且单位元非常清晰。

你可以拿尺规在纸上画一条具体的曲线,然后选两个点实测一下。找一条光滑的椭圆曲线,用直尺连线,肉眼判断第三个交点,再关于x轴对称,检验加出来的点是否还落在曲线上。我当初这么干了很多次,才真正接受这套几何定义不只是一个花架子,它背后有着严密的代数逻辑支撑。

2.2 代数公式:把画图变成坐标运算

几何画法虽然直观,但想要编程实现或者严格计算,就必须落成坐标公式。假设曲线为 y² = x³ + ax + b,且P ≠ Q,令连接P和Q的直线斜率为λ = (y₂ - y₁)/(x₂ - x₁),则P + Q的坐标满足:

x₃ = λ² - x₁ - x₂,y₃ = λ(x₁ - x₃) - y₁。

这里x₃和y₃就是P + Q的结果。看起来简单,但它背后其实走了一遍直线代入曲线方程、解三次方程求根的过程。三次方程的三个根分别是x₁、x₂和x₃,利用韦达定理,就能得到上述公式。

如果是倍点P = Q,令λ = (3x₁² + a)/(2y₁),则:

x₂P = λ² - 2x₁,y₂P = λ(x₁ - x₂P) - y₁。

手动推一遍可能更好理解。把直线方程y = λ(x - x₁) + y₁代入曲线方程,整理后是一个关于x的三次方程。因为已知两根x₁和x₂(或倍点时两根都是x₁),用韦达定理求出第三根x₃,再代回直线方程求出y₃。看到这个推导过程,你就会明白,为什么前面强调曲线必须是三次方程,为什么判别式必须非零,因为一旦退化成带尖点的曲线,切线斜率可能不存在,整个公式组就崩溃了。

2.3 群公理的验证:为什么它真的能构成群

我直接给结论:椭圆曲线上的点集构成一个阿贝尔群,单位元是无穷远点O,逆元是(x, y)对应的(x, -y)。这里我稍稍展开讲,毕竟是“代数结构”这篇文章的核心。

  • 封闭性:两个点相加的结果还是一个点,由公式保证。
  • 交换律:连线不受方向影响,公式也不依赖谁先谁后,当然成立。
  • 结合律:这是最复杂的。几何上可以用九点定理证明,代数上则是暴力计算。让人欣慰的是,结合律确实成立,但绝不是“一眼就能看出来”的。我建议你找一本椭圆曲线教材,看看结合律的证明,哪怕是看个大概也会对群结构有更深理解。
  • 单位元:O + P = P,需要特别处理。如果P是无穷远点或者P和Q是竖直对称点,直接用公式会除零,所以要单独判断。
  • 逆元:P(x, y)的逆元是(x, -y),因为P + (x, -y) = O。

注意到这些点以后,你会理解为什么教科书总要强调“添加无穷远点”。没有无穷远点,群的单位元无法在坐标系里直观呈现,逆元的定义也不完整。从几何上看,无穷远点是所有竖直方向的公共交汇点。从代数射影几何来看,它把仿射平面补全成射影平面,让所有直线“都有交点”,从而让很多论证变得干净利落。

3. 实操环节:用Python亲手实现群运算

数学定义再漂亮,不落在代码里总觉得不够踏实。这一节我分享一段可以直接运行的Python代码,用来在实数域的椭圆曲线上做点加法和倍点运算。虽然密码学中真正使用的通常是有限域上的椭圆曲线,但先搞懂实数的版本,后面转有限域就很容易。

我选的曲线是 y² = x³ - x,其中a = -1,b = 0。这条曲线好处是判别式不为零,且有很多整数点,方便验证。

class EllipticCurvePoint: def __init__(self, x, y, a, b): self.x = x self.y = y self.a = a self.b = b # 检查点是否在曲线上,排除无穷远点的情况 if self.y is not None and self.y**2 != self.x**3 + self.a * self.x + self.b: raise ValueError(f"点({x}, {y})不在曲线 y^2 = x^3 + {a}x + {b} 上") def __eq__(self, other): return self.x == other.x and self.y == other.y and self.a == other.a and self.b == other.b def __add__(self, other): if self.a != other.a or self.b != other.b: raise ValueError("两条不同的曲线上的点不能相加") # 处理无穷远点 if self.y is None: return other if other.y is None: return self # 处理逆元相加 if self.x == other.x and self.y == -other.y: return EllipticCurvePoint(None, None, self.a, self.b) # 倍点公式 if self == other: if self.y == 0: return EllipticCurvePoint(None, None, self.a, self.b) lam = (3 * self.x**2 + self.a) / (2 * self.y) else: lam = (other.y - self.y) / (other.x - self.x) x3 = lam**2 - self.x - other.x y3 = lam * (self.x - x3) - self.y return EllipticCurvePoint(x3, y3, self.a, self.b)

上面这段代码用了直接的操作逻辑,没有重载运算符。使用起来是这样:

# 定义曲线 y^2 = x^3 - x 上的两个点 a, b = -1, 0 P = EllipticCurvePoint(0, 0, a, b) Q = EllipticCurvePoint(1, 0, a, b) # 注意P和Q都在x轴上,它们的和会怎样?

实际测试的时候我建议选一些不在x轴上的点,比如(2, √6)之类的,否则很容易遇到加出来是无穷远点的情况,不利于感受运算过程。比较直观的整数点其实不少,比如(2, -2)?

这里我直接算一个更直观的例子。我选一条更方便验证的曲线 y² = x³ + 2x + 3,上面有一个点P = (0, √3),但为了好算,干脆我编一个带整点的例子:y² = x³ - 7x + 10,点P = (1, 2),点Q = (3, 4)。你可以验证这两个点确实在曲线上。

用上面的公式手算一遍:

λ = (4 - 2) / (3 - 1) = 1,x₃ = 1² - 1 - 3 = -3,y₃ = 1 × (1 - (-3)) - 2 = 2。所以P + Q = (-3, 2)。代入曲线方程验证一下:(-3)³ - 7×(-3) + 10 = -27 + 21 + 10 = 4,而y² = 2² = 4,恰好成立。

为了让你能快速玩起来,我建议把上面的类保存为一个文件,比如ecc.py,然后写一段测试:

P = EllipticCurvePoint(1, 2, -7, 10) Q = EllipticCurvePoint(3, 4, -7, 10) R = P + Q print(R.x, R.y) # 输出应该是 -3, 2

如果你想再体验一下结合律,可以继续取一个点S,然后分别计算(P + Q) + S和P + (Q + S),看看结果是否一致。我当初跑这个验证的时候,心里其实捏了把汗,毕竟理论证明和程序跑出来是两码事,看到输出一致才彻底放心。

3.1 带坐标的类型设计:为什么用类而不是裸元组

我用一个类来表示点,而不是简单的(x, y)元组,是为了把曲线参数a、b一起封装进去,并且可以在构造时校验点是否在曲线上。这是我在实际写代码时踩过坑之后总结的经验。

如果只用裸元组,很容易出现把两条不同曲线的点加到一起的荒唐情况,而且出了错误很难排查。把曲线参数放进点对象里,相当于给每个点挂上了“身份标签”,相加前先检查是否属于同一曲线,代码的自解释性也强很多。

另外,类里我特意定义了__eq__方法,否则Python会默认按对象身份比较,两个坐标相同的点会被当作不同对象。这个坑虽然低级,但新手很容易踩。如果你直接用元组就能天然避免,但元组又没法携带曲线参数,所以两害相权取其轻,我还是选择了类。

3.2 初步测试:跑通最朴素的点加法

我把上面手算的例子跑一遍:

curve_a, curve_b = -7, 10 P = EllipticCurvePoint(1, 2, curve_a, curve_b) Q = EllipticCurvePoint(3, 4, curve_a, curve_b) R = P + Q print(R.x, R.y)

输出应该是-3.0 2.0。这里出现浮点数很正常,因为直线斜率λ通常不是整数。如果你想要高精度计算,可以考虑用 fractions 模块或 sympy,但初学阶段浮点足够。

如果你想玩得更细,可以自己写一个检验函数,判断计算结果仍然落在曲线上:

def assert_on_curve(point, a, b): if point.y is None: return assert abs(point.y**2 - (point.x**3 + a*point.x + b)) < 1e-9

有了这个断言,你可以在每次加法后都做验证,确保程序没有bug。这一步虽然简单,但能帮你建立自信,尤其是后面填了负数、逆元、无穷远点等边界条件之后。

4. 实操中常见的问题与排查技巧

数学上干净利落的群运算,落到代码和手算里总会遇到一堆边界情况。这一节我把自己踩过的坑和排查思路整理成清单,方便你对照着检查。

4.1 无穷远点怎么表示:代码里的“None”与数学里的“O”

无穷远点是椭圆曲线群的单位元。在代码里我选择把它表示为x=None,y=None,并在加法函数里单独处理。这是最容易遗漏的一条分支。

如果你忘了处理无穷远点,那么当P + (-P)时,就会出现除零错误,因为连接两点是竖直线,斜率的分子为0还是分母为0要看你怎么定义。具体来说,如果两个点满足x相等、y互为相反数,代码里如果直接走一般公式,分母x₂ - x₁ = 0,程序直接崩溃。

我的处理方式是先判逆元,再判倍点,最后才走一般公式。排查这类问题的技巧是写一组测试用例,覆盖P + O、O + P、P + (-P)、O + O四种情况,确保输出符合群的公理:

  • P + O = P
  • O + P = P
  • P + (-P) = O
  • O + O = O

4.2 切线斜率不存在的情况:如何正确做倍点

计算2P时,公式要求使用切线斜率λ = (3x₁² + a)/(2y₁)。如果y₁恰好等于0,那就意味着P点在x轴上,此时切线是竖直的,切线与曲线的第三个交点就是无穷远点。数学上2P = O,代码里如果不做判断,分母为0也会崩溃。

所以在倍点分支里,我先判断y是否等于0,如果是就直接返回无穷远点。很多资料里会提到“特征2和特征3的椭圆曲线公式不同”,这里先不用管那么深,但在实数域上,y=0这个边界必须处理。我在代码中已经写了if self.y == 0的判断,就是为了避免这个坑。

4.3 曲线必须非奇异:判别式为何不能为零

三次方程x³ + ax + b如果有重根,曲线就会出现尖点或自交点,这种曲线叫奇异曲线。奇异曲线上的群结构会塌掉,因为几何上“第三个交点唯一性”被破坏了。

判别式Δ = -16(4a³ + 27b²),由于常数-16在特征不为2、3的域上不影响是否为零,一般直接说4a³ + 27b² ≠ 0即可。例如y² = x³这条曲线,a = 0,b = 0,判别式为0,它在原点有一个尖点,不能用椭圆曲线群运算。我在上面代码的构造函数里没有加这个检查,严格来说应该加上。建议你实际使用的时候补一个判断:if 4a**3 + 27b**2 == 0: raise ValueError("奇异曲线")。

排查技巧:当你发现某个点代入曲线恒成立,但加法结果总是怪异时,先检查是不是曲线选错了。我做过一次y² = x³的实验,计算结果乱成一锅粥,后来才回想起是尖点破坏了群律。

4.4 有限域上的计算:为什么实数上没问题,一上密码学就变了

上面所有公式在实数域上都能跑通,但密码学里使用的却是有限域上的椭圆曲线,记作GF(p)或GF(2^m)。原因很简单:实数域上的点有无穷多个,计算机没法枚举,也无法保证计算难度。而有限域上的点虽然也多,但有限,且离散对数问题比有限域乘法更难以拆解。

从实数域切换到有限域,核心公式基本不变,只是把加、减、乘、除换成模运算。除法的逻辑要改成模逆运算,比如计算斜率λ = (y₂ - y₁)/(x₂ - x₁)时,分母实际上是乘以它的模逆元。我建议你先在实数域把代码调通,然后再扩展成模算术版本。如果直接上有限域,一旦出bug,很难分辨是数学问题还是编码问题。

这里给一个模逆运算的实现提示。设模为p,用扩展欧几里得算法求inv(a, p),然后乘法代替除法。如果使用Python 3.8以上版本,可以直接用pow(a, -1, p)。

4.5 快速排查清单

我把自己排查问题时常用的检查项整理成一个表:

症状可能原因检查方法
除零错误两个点x坐标相同但y相反在一般加法前判断逆元
结果不在曲线上曲线判别式为0或构造函数没校验检查4a³ + 27b²是否为零
单位元错乱没有处理无穷远点或无穷远点表示冲突跑一遍P + O、O + P的用例
倍点结果异常y=0时未做边界处理测试x轴上交点的倍点
结合律不成立可能用了错误曲线参数随机取3个点,验证结合律

5. 群结构为何重要:从数学结构到密码学应用

讲完了群运算和代码实现,我想把视野拉高一点,谈谈这个代数结构到底有什么用。为什么金融系统、数字签名、区块链密钥体系里都能看到椭圆曲线的影子?

关键在于一个问题:给定曲线上一点P和一个整数k,计算kP是很容易的,就是从P开始反复加法。但反过来,给你P和kP,让你反推出k,这就非常难了。这个问题叫椭圆曲线离散对数问题(ECDLP)。它的“难”与群结构本身密切相关。在实数域上,这个反推问题没有密码学意义,因为点坐标是浮点数,误差会放大;但在有限域上,点的集合是有限的,运算无法借助实分析的逼近手段,目前也没有多项式时间算法能解决,暴力尝试又是天文数字级别的规模。

所以你会发现,群结构提供了“加法”这种运算方式,而有限域提供了“离散”的集合。两者结合,才催生了ECC密码体制。经典场景是密钥协商:双方各自选一个私钥k₁和k₂,公开参数P,然后互相交换k₁P和k₂P,双方都能算出共享密钥k₁k₂P,但窃听者拿不到k₁和k₂,无法高效算出同样的值。

需要特别注意的是,密码学中使用的曲线参数都是经过严格挑选的,并非随便拿一条曲线就能用于安全通信。实际系统里常见的曲线如P-256、secp256k1,都是经过特殊设计,避开弱曲线以及各种攻击面。

除了密码学,椭圆曲线的群结构在数论里还有更深的用处。一个经典结论是:定义在有限域上的椭圆曲线的点数N,满足 |N - (p+1)| ≤ 2√p,这叫做Hasse界。这个结论之所以重要,是因为它告诉我们曲线上的点不会太多也不会太少。点数的精确计算算法(如Schoof算法)就是基于群结构做各种模运算和除法,最后推出精确点数。你看,从一条曲线到一个群,再到计数、协议设计,整个故事线都串起来了。

5.1 为什么有限域上的点构成有限群

实数域上椭圆曲线有无数个点,怎么换到有限域变成有限群呢?因为有限域GF(p)本身就只有p个元素,点的坐标(x, y)都落在GF(p)里,所以最多只有p²个点,再加无穷远点,总数有限。每两个点加法结果仍然落在这个有限集里,所以它天然就是有限群。

理解这个转变之后,你就会明白一个很微妙的事情:有限域上的椭圆曲线图形不再是连续曲线,而是一堆离散点组成的集合。所谓“曲线”只是一个名字,真实场景里是一堆满足方程的点的集合。这一点初学者特别容易误解,以为密码学里用的“椭圆曲线”还是一条可以画出来的平滑曲线,其实根本不是。我建议你找个小素数p=17,枚举一下y² = x³ + 2x + 3在GF(17)上的点,亲眼看到点集是离散的,才能真正接受这个事实。

5.2 结合律在密码学中的意义:标量乘法能够安全展开

密码学里计算kP时,通常使用“倍增与累加”的方法:把k写成二进制,从高往低扫描,每次对当前点做倍点,遇到1就多加一个P。这个过程之所以成立,完全依赖群运算的结合律。如果结合律不成立,简单的重复加法就会出现歧义,根本没法安全展开。

你可能觉得“结合律是群的基本要求,有什么可稀罕的”。但在椭圆曲线上,结合律并不是免费的午餐,正是这套几何加法的精妙之处。撰写标准库、设计加密协议的工程师们,最先验证的往往就是参数曲线上的群运算是否正确实现,因为他们知道,任何一点微小的错误都会破坏这个基础。

6. 写在最后:我踩过的一些坑和给你的练习建议

聊了这么多,关于椭圆曲线和代数结构的美妙之处,我还有一个很深的体会:很多人一上来就学有限域上的群运算,结果被一堆模运算细节淹没,反而忘了背后的几何直觉。我的建议是,先在实数域上用“连线取交点再对称”的方式理解群运算,手推两三个例子,再用代码验证,最后再切换到有限域。这个顺序能帮你把抽象代数中的群概念,牢牢锚定在一个可见、可算的几何对象上。

我当年最容易混淆的概念是,群运算的单位元为什么是无穷远点而不是(0, 0)。后来我画了很多图,又手工算了几次P + (-P),才彻底明白。对初学者来说,这部分需要用一点耐心,因为它实在太反直觉了。如果一次没理解,不妨把手算的每一步写清楚,尤其是处理无穷远点的分支,别跳过。

至于后续扩展,你可以从两条路线继续往下走。一条是往代数数论方向,研究椭圆曲线的秩、Torsion子群、Mordell定理;另一条是往应用密码学方向,去读secp256k1的参数文档,自己实现一个有限域上的点加法和标量乘法,然后跑一遍密钥协商协议。这两条路线都很能锻炼人,而且会不断加深你对“代数结构如何赋能实际问题”的理解。

最后再分享一个小技巧:当你写代码或者手算遇到奇怪的不一致时,先用“结合律”做一次冒烟测试。随机生成三个点P、Q、R,验证(P + Q) + R等于P + (Q + R)。如果这一步都过不了,其他任何结论都不可信。群运算就像一台精密的机器,结合律是它的轴承,一旦轴承坏掉,整台机器都会散架。先保住这个核心,你后续的探索会顺畅很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询