结式与多项式 GCD
在我还没高考的时候,我写过这个博客,一道困扰我至今的题目,在辗转许久后,我终于有了一个比较满意的答案。于是这是一篇很杂乱的博客。
实际上这个博客应该在上一篇之前写,因为我从来没有认识到过多项式 GCD 本身就是一个值得探究的问题。
结式(Resultant)
结式是研究两个多项式之间是否有公共根的一个工具。假如两个多项式 \(f(x), g(x)\) 有公共根,那么我们知道他们的 \(\gcd\) 不为 \(1\)。考虑到如果 \(\gcd(f, g) = 1\),那么我们根据扩展欧几里得算法可以得知存在多项式 \(u, v\) 使得 \(u f + v g = 1\)。
注意这个过程实际上是很线性的,如果我们把 \(u, v\) 的系数写成一个向量,那么结果的多项式是可以写成一个关于 \(f, g\) 两个多项式的系数的矩阵的。具体的,令 \(\mathrm{deg}(f) = n, \mathrm{deg}(g) = m\), \(f, g\) 的系数为 \(f_i, g_i\),那么对于多项式 \(\mathrm{deg}(u) < m, \mathrm{deg}(v) < n\),我们把 \(u, v\) 的系数写成一个向量 \(\mathbf{c} = (u_0, u_1, \ldots, u_{m-1}, v_0, v_1, \ldots, v_{n-1})\),那么 \(u f + v g\) 的系数就可以写成一个关于 \(f_i, g_i\) 的矩阵乘以 \(\mathbf{c}\) 的形式:
(把矩阵分成左右两边,实际上是不对齐的,不过懒得改 LaTeX 了,)
总之就是把系数进行若干次位移得到这个矩阵。我们把上面的矩阵称为 \(f, g\) 的 Sylvester 矩阵,记为 \(\mathrm{Syl}_x(f, g)\)。
那么我们就可以考察什么时候存在 \(u, v\) 使得 \(uf + vg = 1\) 了,也就是考察什么时候 \(\mathrm{Syl}_x(f, g) \mathbf{c} = (1, 0, \ldots, 0)^t\) 有解了。让我们考察这个矩阵的行列式,如果这个行列式不为 \(0\),那么显然方程是有解的,那么 \(f, g\) 的 \(\gcd\) 就是 \(1\),反之如果行列式为 \(0\),我们实际上可以找到一个非零的 \(\mathbf{c}\) 使得 \(\mathrm{Syl}_x(f, g) \mathbf{c} = \mathbf{0}\),也就是说存在 \(u, v\) 使得 \(uf + vg = 0\),考虑到我们限制了 \(u, v\) 的次数,这使得不存在平凡解 \(u = -g, v = f\),此时 \(u\) 不可能覆盖掉 \(g\) 的所有因子,一定有一个因子来自于 \(f\),则 \(f, g\) 一定有一个不为 \(1\) 的 \(\gcd\)。
因此我们得到了一个结论:\(f, g\) 的 \(\gcd\) 不为 \(1\) 当且仅当 \(\mathrm{Syl}_x(f, g)\) 的行列式为 \(0\)。我们把 \(\mathrm{Syl}_x(f, g)\) 的行列式称为 \(f, g\) 的结式,记为 \(\mathrm{Res}_x(f, g)\)。
此时我们就得到了一种判定两个多项式是否存在公共根的方法,只需要计算两个多项式的结式是否为 \(0\) 就可以了。这也给我们了一些有趣的数论上的性质,如果 \(f, g\) 的系数都是整数,那么 \(\mathrm{Res}_x(f, g)\) 也是一个整数,此时我们可以考虑在模某个 \(\mathrm{Res}_x(f, g)\) 的因子的意义下,\(f, g\) 是否存在公共根,因而我们可以得到 \(\gcd(f(k), g(k))\) 的取值一定是 \(\mathrm{Res}_x(f, g)\) 的一个因子。(注意这里是多项式取值的 \(\gcd\),而不是多项式的 \(\gcd\))
实际上,这个矩阵的行列式还可以写成根的形式。显然这个行列式是关于系数的多项式,而根据韦达定理,我们又可以把他化成关于根的多项式。实际上可以通过一些仔细的计算,发现这个行列式关于根是一个齐次多项式,且次数为 \(nm\)。(可以看作每行选一个,然后每到下一行所有的次数都会 +1,于是初值加增量可以计算出 \(-\frac{m(m-1)}{2}-\frac{n(n-1)}{2} + \frac{(n+m)(n+m-1)}{2} = nm\))而由于当存在重根时这个多项式会等于 \(0\),他一定存在 \((\alpha_i - \beta j)\) 的因子,于是我们可以直接得到结式的一个形式:\(\mathrm{Res}_x(f, g) = C \prod_{i=1}^n \prod_{j=1}^m (\alpha_i - \beta_j)\)。而 \(C\) 这个常数可以代特殊情况计算出来(比如 \(f = a x^n, g = b (x+1)^m\)),最终我们可以得到 \(\mathrm{Res}_x(f, g) = a^m b^n \prod_{i=1}^n \prod_{j=1}^m (\alpha_i - \beta_j)\)。
这东西还有另一个作用,就是对于多元多项式来说,我们可以通过结实消去某一个元。比如对于 \(f(x, y), g(x, y)\) 来说,我们可以把 \(y\) 看成一个常数,那么 \(f, g\) 就是关于 \(x\) 的多项式了,此时我们就可以计算 \(\mathrm{Res}_x(f, g)\),这个结果是一个关于 \(y\) 的多项式,如果这个多项式不为 \(0\),那么说明 \(f, g\) 没有公共根,也就不可能是解,反之如果 \(\mathrm{Res}_x(f, g)(y_0) = 0\),那么说明 \(f(x, y_0), g(x, y_0)\) 有公共根,我们就可以通过这个公共根来构造出 \(f, g\) 的一个公共解了。
多项式 GCD
我一直以为多项式 GCD 是个很显然的事情,不过当我做这个题的时候发现还是有很多问题的。
对于整数域上的 \(\gcd\),我们可以通过唯一分解的方式来显然的定义。而对于多项式上来说,我们有了一个问题:多项式部分可以进行分解,但是他们还带着一个系数,这个系数是可以被任意非零常数乘的。一种定义方法是将多项式的所有系数求一个 \(\gcd\),把这部分除掉,然后剩下的常数再整个进行分解,这样就给出了一个多项式的唯一分解。
可是多项式求 \(\gcd\) 如果按照辗转相除的方法来求,就会出现一些很诡异的情况。一个最大的问题就是,得到的很可能不是整数。比如 \(3x^2 + 1\) 和 \(2x^2 - 1\),这时候取模就炸裂了,一定会出现分数。实际上我们知道,分数域是欧几里得域,所以我们是可以在分数域上进行辗转相除的,而我们实际上相求的是整数多项式的 \(\gcd\),这中间就需要进行一些转换。细节就懒得写了(因为我也没仔细看),总之就是可以先进行一些转换,变成在分数域下求出 \(\gcd\),最后再乘一些常数变成整数多项式的 \(\gcd\)。
可是分数是很不好用的,因为通分会使得分母迅速膨胀,导致计算起来复杂度可能会很高。这时候我们可以考虑在模意义下进行运算。但是取什么模数呢?首先是需要防止多项式退化,所以模数不能是两个多项式的最高次系数的 \(\gcd\) 的因子。其次,我们需要防止本来不应该出现在 \(\gcd\) 中的因子出现在 \(\gcd\) 中。例如,如果有两个多项式分别有因子 \((x - 3)\) 和 \((x - 5)\),如果在 \(\bmod \ 2\) 下,这两个因子就变成了相同的因子。于是,我们需要避免模数是某两个根的差的因子。于是,这就和刚才所讨论的结式联系了起来,我们只需要让模数不是结式的一个因子即可。
其他的东西
在这个研究过程中,其实还碰到了一个东西叫做“子结式”,但是具体细节我没有很仔细研究。他应当是在求一个多项式 \(h\) 的序列,满足存在 \(uf + vg = h\),而在这样一个算法中求到的次数最小的非 \(0\) 多项式就是这两个多项式的 \(\gcd\) 了。所以这本质是一个不需要分数的多项式 \(\gcd\) 算法。而事实上,这个算法最后得到的 \(0\) 次的结果就是结式(的绝对值?)
还有一件事情,就是在模合数意义下的多项式 \(\gcd\)。实际上很多问题在模合数下是不太好的,一般的做法是先把合数分解成质数的乘积,然后在模每个质数下进行计算,最后再 CRT 合并起来。但是我们需要考虑到多项式分解是很困难的事情,这导致有些时候这个事情是很困难的。对于多项式 \(\gcd\) 而言,由于辗转相除需要涉及到系数的除法,如果在模质数的情况下是一定存在逆元的,但是在模合数下有可能会不存在逆元。不过这其实是一个两面性的事情:我们可以先把一些较小的因子除掉,现在剩下的模数因子一定是很大的。如果真的遇到了逆元不存在的情况,这种情况是 \(\gcd(a, m) \ne 1\),发现这其实把 \(m\) 分解了。这种情况概率是很小的,所以很多情况下我们可以认为这种情况不会发生,但是即便发生了也是可以处理的。
这也导致 sage 下 gcd 是没法用的,因为这不是一个确定的做法。不过一个做法就是上面所说的,先把小因子全去掉,然后剩下的我们可以直接手动跑欧几里得算法,或者我们可以用这个 f.subresultants(g)[0] 来求。
Franklin–Reiter attack
这个就是一个相关的攻击,假如两个 message 之间是一个线性关系,我们就可以通过一些多项式 GCD 来把 message 找出来。具体来讲,如果 \(m_2 = f(m_1)\) 是关于 \(m_1\) 的一个小多项式(实战中通常碰到的都是线性,不过二次三次等等也无所谓),我们有两个密文,那么实际上就是知道一组方程 \(m_1^e - c_1 \equiv 0 \pmod{n}\) 和 \(f(m_1)^e - c_2 \equiv 0 \pmod{n}\),那么我们就是在求这两者的公共根。如果这时候 \(e\) 比较小,那么就可以大力求 \(\gcd\),然后就能直接解出 \(m_1\)。可以直接手写 \(\gcd\),也可以使用上面所说的 f.subresultants(g)[0] 技巧来求。\(e\) 小是为了保证 \(\gcd\) 能跑得动。
一个小 demo:
from sage.all import *
from Crypto.Util.number import getPrime
while True:
try:
p = getPrime(512)
q = getPrime(512)
n = p * q
e = 1003
phi = (p - 1) * (q - 1)
d = inverse_mod(e, phi)
except Exception:
continue
else:
break
m1 = randint(1, n - 1)
a = 3
b = 114514
m2 = (a * m1 ** 2 + m1 + b) % n
c1 = power_mod(m1, e, n)
c2 = power_mod(m2, e, n)
R = PolynomialRing(Zmod(n), 'x')
x = R.gen()
f = x ** e - c1
g = (a * x ** 2 + x + b) ** e - c2
gg = f.subresultants(g)[0]
m = -gg[0] / gg[1]
assert m == m1
print("successfully recovered m1")
当然有时候你不知道这个偏移是什么,假如线性关系很简单(比如 \(m_2 = m_1 + r\)),那么我们可以直接把这个 \(r\) 当作第二个未知数。还记得前面说的消元吗?我们可以直接消掉 \(m\) 留下一个关于 \(r\) 的多项式,而 \(r\) 很小的话就可以用 Coppersmith 来解了。
这个我没自己写,贴一个代码在这里吧。(Source:Github Samuel Groß)
def coppersmith_short_pad(c1, c2, n):
"""When provided with two ciphertexts of the same message but with different (short)
paddings, i.e.
C1 = (M | r1)^3 = M1^3 and
C2 = (M | r2)^3 = M2^3),
this function calculates the difference r2 - r1 of the two (random) paddings used
during encryption using Coppersmith's method. Afterwards, the Franklin-Reiter
attack can be applied to recover the plaintext.
See http://www.di.ens.fr/~fouque/ens-rennes/coppersmith.pdf
Returns all possible values for the difference in a list.
"""
# Fixed exponent.
e = 3
# Create two polynomial rings and corresponding variables. Sage cannot calculate the resultant
# in a ring of integers modulo N, so we'll use and integer ring for that, then switch rings afterwards.
RZmodN.<xm> = PolynomialRing(Zmod(n))
RZ.<x, y> = PolynomialRing(ZZ)
# Create the two polynomials g1 and g2. If y = r2 - r1, then g1 and g2 both have the root M1.
g1 = x**e - c1
g2 = (x + y)**e - c2
# Calculate the resultant. The resultant is the product of the differences of the roots of the
# two polynomials. As seen above, for the case that y = r2 - r1, both polynomials have the same
# root M1, thus r2 - r1 is a root of the resultant of g1 and g2.
p = g1.resultant(g2)
#
# Now let sage do all the work by calculating small roots of the resultant.
#
# We need a univariate polynomial for this. At this point, x should be eliminated though.
p = p.univariate_polynomial()
# Change rings. We need the ring of integers modulo N now.
p = p.change_ring(RZmodN).subs(y = xm)
# Make sure the polynomial is monic, i.e. the coefficient of x^(p.degree()) is 1.
p = p.monic()
# Now let sage to the magic. Try to find all small roots of p using Coppersmith's method.
return p.small_roots(X=2**(512 // 9), beta=0.5)

浙公网安备 33010602011771号