- 已编辑
原文链接:https://tover.xyz/p/HSSP-note/
看看能不能抢到2024年的最后一篇帖子
以下正文:
现在是个人都会用正交格攻击了,搞得我不学好像就落后似的,所以抽空学习了一下
其中,和正交格相关的最出名的就是HSSP问题了,于是下面就把HSSP问题怼一遍
HSSP问题
HSSP(Hidden Subset Sum Problem)问题大概如下
令为大整数,整数,向量为维向量,且的元素落在中,令
现知道和,求和
PS:根据Coron的测试数据,M的大小大概是,其中,(也有)
和经典背包问题(SSP)的区别是,在HSSP中隐藏(Hidden)了,所以无法直接通过构造格来解
给一个生成问题的样例代码
def genHssp(m, n, M):
R.<z> = PolynomialRing(Zmod(M))
x = [R([randint(0, 1) for mi in range(m)]) for ni in range(n)]
a = [randint(0, M-1) for ni in range(n)]
h = sum([a[i] * x[i] for i in range(n)])
return (a, [xi.list() for xi in x]), (M, h.list())
m = 200
n = 100
M = 199999999999997
(a, X), (M, h) = genHssp(m, n, M)
线性代数知识
在讲正交格前先来补充一点线性代数的前置知识,更详细的内容可以参考
Strang, Gilbert. Introduction to linear algebra. Wellesley-Cambridge Press, 2022.
讲到线性代数,第一个想到的应该都是像
这样的矩阵和向量的运算
如果不把矩阵和向量看成单纯的数字,而是看成是空间和空间中的点的话,就可以得到著名的四大基本子空间:行空间、列空间、零空间(核空间)和左零空间
给定一个(行列)的矩阵,可以把的每一列看成空间的基,用这个行向量的基张成(Span,即进行线性组合)的空间就叫列空间,数学方式表示大概是(如果是格的话就是)
如果把的每一行看作空间的基,那么张成的空间就叫行空间,数学表示大概是
如果只关注这个方程,那么方程的所有解落在零空间(又叫核空间)中
如果把放在的左边,得到的空间又叫左零空间
令为矩阵的秩(Rank),那么列空间和行空间的维度都是,零空间的维度是,左零空间的维度是
直观上看,消元后非零的列和行的数量都是,零的列是,零的行是,详细的证明可以看书或者网上找找
在四大基本子空间中有一个重要的结论是,行空间与零空间相互垂直,列空间与左零空间相互垂直
可以简单证明一下,令为行空间的一个向量,令为零空间的一个向量,那么两个向量相乘
根据零空间的性质,,所以
也就是任意一个行空间的向量与任意一个零空间的向量都相互垂直,即行空间与零空间相互垂直
列空间与左零空间的证明类似
另一个重要的结论是,行空间与零空间可以张成整个空间,列空间与左零空间可以张成整个空间
直观上看,行空间与零空间相互垂直就是不相关,然后两个空间的维度加起来刚好是
列空间与左零空间的也类似
由这两个结论可得,如果要求一个格的正交格(就是相互垂直的),那么只要求他的零空间(行看作基)或者左零空间(列看作基)就好
Flatter
Flatter是一个比LLL更快的格规约算法
和LLL不同的是,目前SageMath没有原生集成Flatter,所以需要装一个
安装方法可以直接看Github,大概就是
git clone https://github.com/keeganryan/flatter.git
sudo apt install libgmp-dev libmpfr-dev fplll-tools libfplll-dev libeigen3-dev
mkdir build && cd ./build
cmake ..
make -j8
# 软链接路径改成自己的PATH
ln -s `pwd`/bin/flatter [imath:0]HOME/.local/bin
flatter -h
然后我直接抄了@Neobeo的做法,通过子进程调用Flatter二进制应用
# https://github.com/Neobeo/HackTM2023/blob/main/solve420.sage
# faster LLL reduction to replace `M.LLL()` wiith `flatter(M)`
def flatter(M):
from subprocess import check_output
from re import findall
M = matrix(ZZ,M)
# compile https://github.com/keeganryan/flatter and put it in [/imath:0]PATH
z = '[[' + ']\n['.join(' '.join(map(str,row)) for row in M) + ']]'
ret = check_output(["flatter"], input=z.encode())
return matrix(M.nrows(), M.ncols(), map(int,findall(b'-?\\d+', ret)))
在SageMath中用的时候直接把正常用的M.LLL()
换成flatter(M)
即可
HSSP问题的格通常都比较大,所以用Flatter会比LLL节约不少时间
Nguyen-Stern算法
接下来就看看这个HSSP到底要怎么解,以下内容我参考了
Coron, Jean-Sébastien, and Agnese Gini. "A polynomial-time algorithm for solving the hidden subset sum problem." Annual International Cryptology Conference. Cham: Springer International Publishing, 2020.
还有文章对应的代码,和@tl2的文章,有很多被我忽略掉的内容都可以在这篇文章中看到
Nguyen和Stern的做法是,给定,首先找与垂直的向量,那么就有
令向量
那么问题就可以转化为
也就是和在模的情况下相互垂直
然后如果是短向量的话,那么也会是短向量(因为的元素落在中),如果比所有与垂直的非零向量都短的话,那么就只能是
而如果的话,就是,令是以为基的格,就可以得到,即是的正交格中的向量
的维度是:因为的基的秩是(),然后我这里是看成行向量为基的空间(行空间),且的基是行列的,所以根据前面的线性代数知识,与行空间垂直的零空间的维度就是
所以,如果我们可以找到个满足条件的向量的话,就相当于找到了的正交格,进而使用找到,最后由恢复基
于是,最后得到的攻击路线就是
- 用构造格基,LLL找到个短向量
- 用构造格,用找的正交补(可以看作是和同一个空间,但基不是)
- 对使用BKZ恢复
Part.1 找短向量u
这里我直接用Coron论文中的方法造格
首先拆开
得到
然后提出其中的
两边乘
最后拆开模数,并换一下位置
根据这个关系就可以构造格基
令
那么就是
根据Coron文章第三章的分析,可以保证对规约后的前行是满足条件的向量,这个,可以自己看论文...
B = matrix(ZZ, m)
B[0, 0] = M
h0i = Integer(h[0]).inverse_mod(M)
for i in range(1, m):
B[i, 0] = - h[i] * h0i
B[i, i] = 1
L = flatter(B)
vh = vector(Zmod(M), h)
print([vector(Zmod(M), list(l)) * vh for l in L])
另外,还可以构造另一种更直观的格基
令
那么就是
这个格基在Coron的文章和@tl2的文章都有类似的,可以去参考一下
Part.2 恢复格Lx
这一步就比较简单
首先根据上面分析,用L
的前m-n
就可以构造
然后只需要求的零空间就可以得到的正交补
这里我直接用SageMath的right_kernel
求令空间,亲测把algorithm
指定为pari
的话会快一点
Lxo = matrix(ZZ, L[:m-n])
Lxc = Lxo.right_kernel(algorithm='pari').matrix() # faster
print('right_kernel done.')
Lx_real = matrix(ZZ, [xi + [0] * (m - len(xi)) for xi in X])
rsc = Lxc.row_space()
print([xi in rsc for xi in Lx_real])
Part.3 恢复xi
理论上直接对Lxc
求个LLL或者BKZ就可以恢复,但实际上并没有
细看一下,的元素在中,这在01背包问题中也遇到过类似的问题,所以可以利用类似的解决方法,即把转化为
就可以把元素转化到中
虽然这对向量长度影响不大,但乘上去的系数会增大格基的行列式,就更容易筛掉无关的变量
于是就可以构造这样一个格基(其中是元素全为、大小和一样的矩阵)
令(U是,看作一种映射就好)
可以得到关系
所以对归约后就可能得到
进一步观察发现其实中的的每一行都是相关的(甚至相同的),实际作用的就一行,对规约后也发现有行全为
所以不妨令为全为的行向量,就可以把格简化为
参考代码
def checkMatrix(M, wl=[-1, 1]):
M = [list(_) for _ in list(M)]
ml = list(set(flatten(M)))
logging.debug(ml)
return sorted(ml) == sorted(wl)
e = matrix(ZZ, [1] * m)
B = block_matrix([[-e], [2*Lxc]])
Lx = B.BKZ()
assert checkMatrix(Lx)
assert len(set(Lx[0])) == 1
最后恢复一下和
Lx = Lx[1:]
E = matrix(ZZ, [[1 for c in range(Lxc.ncols())] for r in range(Lxc.nrows())])
Lx = (Lx + E) / 2
Lx2 = []
e = vector(ZZ, [1] * m)
rsc = Lxc.row_space()
for lx in Lx:
if lx in rsc:
Lx2 += [lx]
continue
lx = e - lx
if lx in rsc:
Lx2 += [lx]
continue
print('Something wrong?')
Lx = matrix(Zmod(M), Lx2)
vh = vector(Zmod(M), h)
va = Lx.solve_left(vh)
PS:其实用做格也可以,但是干扰的那一行就不会放在第一行,还要另外写代码找出来(就是全为1
或者全为-1
的行)
模板/参考代码
最后把上面所有的代码整合一下
import logging
logging.basicConfig(
level=logging.DEBUG,
format="[%(levelname)s] %(message)s"
)
# https://github.com/Neobeo/HackTM2023/blob/main/solve420.sage
# faster LLL reduction to replace `M.LLL()` wiith `flatter(M)`
def flatter(M, **kwds):
from subprocess import check_output
from re import findall
M = matrix(ZZ,M)
# compile https://github.com/keeganryan/flatter and put it in [imath:0]PATH
z = '[[' + ']\n['.join(' '.join(map(str,row)) for row in M) + ']]'
ret = check_output(["flatter"], input=z.encode())
return matrix(M.nrows(), M.ncols(), map(int,findall(b'-?\\d+', ret)))
def genHssp(m, n, M):
R.<z> = PolynomialRing(Zmod(M))
x = [R([randint(0, 1) for mi in range(m)]) for ni in range(n)]
a = [randint(0, M-1) for ni in range(n)]
h = sum([a[i] * x[i] for i in range(n)])
return (a, [xi.list() for xi in x]), (M, h.list())
def checkMatrix(M, wl=[-1, 1]):
M = [list(_) for _ in list(M)]
ml = list(set(flatten(M)))
logging.debug(ml)
return sorted(ml) == sorted(wl)
def Nguyen_Stern(h, m, n, M):
B = matrix(ZZ, m)
B[0, 0] = M
h0i = Integer(h[0]).inverse_mod(M)
for i in range(1, m):
B[i, 0] = - h[i] * h0i
B[i, i] = 1
#L = B.BKZ() # slooooooow
L = flatter(B)
logging.info('flatter done.')
'''
vh = vector(Zmod(M), h)
logging.debug([vector(Zmod(M), list(l)) * vh for l in L])
'''
Lxo = matrix(ZZ, L[:m-n])
Lxc = Lxo.right_kernel(algorithm='pari').matrix() # faster
logging.info('right_kernel done.')
'''
try:
Lx_real = matrix(ZZ, [xi + [0] * (m - len(xi)) for xi in X])
rsc = Lxc.row_space()
logging.debug([xi in rsc for xi in Lx_real])
except:
pass
'''
e = matrix(ZZ, [1] * m)
B = block_matrix([[-e], [2*Lxc]])
Lx = B.BKZ()
logging.info('BKZ done.')
assert checkMatrix(Lx)
assert len(set(Lx[0])) == 1
Lx = Lx[1:]
E = matrix(ZZ, [[1 for c in range(Lxc.ncols())] for r in range(Lxc.nrows())])
Lx = (Lx + E) / 2
Lx2 = []
e = vector(ZZ, [1] * m)
rsc = Lxc.row_space()
for lx in Lx:
if lx in rsc:
Lx2 += [lx]
continue
lx = e - lx
if lx in rsc:
Lx2 += [lx]
continue
logging.warning('Something wrong?')
Lx = matrix(Zmod(M), Lx2)
vh = vector(Zmod(M), h)
va = Lx.solve_left(vh)
return Lx, va
# stolen from https://github.com/tl2cents/Implementation-of-Cryptographic-Attacks/blob/main/MultivariateHSSP/A%20Polynomial-Time%20Algorithm%20for%20Solving%20the%20Hidden%20Subset%20Sum%20Problem.ipynb
def derive_M(n):
iota=0.035
Mbits=int(2 * iota * n^2 + n * log(n,2))
M = random_prime(2^Mbits, proof = False, lbound = 2^(Mbits - 1))
return Integer(M)
m = 200
n = 100
M = derive_M(n)
(a, X), (M, h) = genHssp(m, n, M)
logging.debug('m: %d | n: %d' % (m, n))
logging.debug('%s, %s' % (M, M.nbits()))
Lx, va = Nguyen_Stern(h, m, n, M)
print(sorted(va) == sorted(a))
M
的生成采用了Coron的 ,偷懒了一下直接偷@tl2的derive_M