Montgomery 模乘¶
背景¶
在密码学中,经常会涉及到模乘操作:
但由于此时的
定义¶
Montgomery 模乘是一种提高模乘的性能的方法。具体地,Montgomery 模乘需要一个参数
本文的内容已经整合到知识库中。
用途¶
这看起来很奇怪,为什么要在原来的
但是,这种计算方法会引入额外的
此时所有数都自带一个
得到的结果是
因此,如果要用 Montgomery 模乘来进行加速,需要经过三个步骤:
- 第一步,把所有数都进行 Montgomery 模乘,乘以
,把所有数都添加上 的系数 - 第二步,按照正常的流程计算,所有值都带有
系数,同时所有的模乘都被替换为了更加高效的 Montgomery 模乘 - 第三步,把结果和
进行 Montgomery 模乘,还原为真实值
因此,只要中间计算的过程是大头,初始化和最后的处理时间就可以忽略,可以享受到 Montgomery 模乘带来的性能提升。
算法¶
接下来介绍 Montgomery 的具体算法,看看它如何提高模乘性能。
REDC¶
首先介绍 Montogomery 的 REDC 算法,它的步骤是:
- 预先计算
,满足 - 计算
- 计算
- 计算
- 如果
,那么 - 否则
可以看到这个过程中只涉及到关于
首先计算过程中出现了
说明
说明
根据
因此
大整数 Montgomery 模乘¶
实际在计算机上运行 Montgomery 算法的时候,由于这些数都很大,因此为了表示大整数,需要用固定位数的整数数组来表示,例如用多个 64 位整数来表示一个大整数。此时,把大整数运算拆成多个 64 位整数的运算,然后把大整数的运算和 Montgomery 模乘结合在一起,得到更高性能的 Montgomery 模乘。
论文 Analyzing and Comparing Montgomery Multiplication Algorithms 分析了几种混合了大整数运算和 Montgomery 模乘的算法。下面讲解论文中提到的部分算法。
在下面的讨论中,假设机器整数的宽度是
Separated Operand Scanning¶
第一种方法是 Separated Operand Scanning 方法(同时也是 Wikipedia 中提到的 MultiPrecisionREDC 算法),它的步骤是:
第一步:按照传统方式进行大整数乘法,计算出
# t = a * b
for i=0 to s-1
C := 0
for j=0 to s-1
(C, S) := t[i+j] + a[j]*b[i] + C
t[i+j] := S
t[i+s] := C
得到的结果放在
第二步,求
# m = ((T % R) * N') % R
for i=0 to s-1
C := 0
for j=0 to s-i-1
(C, S) := m[i+j] + t[i]*n'[j] + C
m[i+j] := S
接下来求大整数
# t += m * N
for i=0 to s-1
C := 0
for j=0 to s-1
(C, S) := t[i+j] + m[i]*n[j] + C
t[i+j] := S
ADD (t[i+s], C)
这里的 ADD 函数指的是大整数加法运算里面,求和后不断进位直到不再进位为止的函数,这里就不展开了。
在这里有一个重要的优化:实际上,不需要把
第一步:m_1 = t[0] * n'[0]
,舍去溢出的部分。
第二步:m_2 = t[1] * n'[0]
。
这个过程可以一直继续下去,每一步的 m_i
都可以用 m_i = t[i] * n'[0]
计算。因此不再需要先求
# t += m * N
for i=0 to s-1
C := 0
# W = 2^w
m := t[i] * n'[0] mod W
for j=0 to s-1
(C, S) := t[i+j] + m*n[j] + C
t[i+j] := S
ADD (t[i+s], C)
计算出 t
数组的低
最后再用大整数减法和比较,使得结果
# t = a * b
for i=0 to s-1
C := 0
for j=0 to s-1
(C, S) := t[i+j] + a[j]*b[i] + C
t[i+j] := S
t[i+s] := C
# t += m * N
for i=0 to s-1
C := 0
# W = 2^w
m := t[i] * n'[0] mod W
for j=0 to s-1
(C, S) := t[i+j] + m*n[j] + C
t[i+j] := S
ADD (t[i+s], C)
# u = t / R
for i=0 to s
u[j] := t[j+s]
# return u or u - N
B := 0
for i=0 to s-1
(B,D) := u[i] - n[i] - B
t[i] := D
(B,D) := u[s] - B
t[s] := D
if B=0 then
return t[0], t[1], ... , t[s-1]
else
return u[0], u[1], ... , u[s-1]
Coarse Integrated Operand Scanning¶
第二种算法 Coarse Integrated Operand Scanning 是在 Separated Operand Scanning 的基础上,把 t
数组的时候,只会依赖已经计算出来的部分。同时,每次循环结束的时候就把整个 t
数组右移一次,因此原来的 t[i]
就会变成 t[0]
,t[i+j]
变成 t[j]
。
for i=0 to s-1
# t = a * b
C := 0
for j=0 to s-1
(C, S) := t[j] + a[j]*b[i] + C
t[j] := S
(C, S) := t[s] + C
t[s] := S
t[s+1] := C
# t += m * N
C := 0
# W = 2^w
m := t[0] * n'[0] mod W
for j=0 to s-1
(C, S) := t[j] + m*n[j] + C
t[j] := S
(C, S) := t[s] + C
t[s] := S
t[s+1] := t[s+1] + C
# t /= W
for j=0 to s
t[j] := t[j+1]
# return t or t - N
# save as above, omitted
每次循环都右移一次,那么循环 t += m * N
和移位两步合并在一起进行:
for i=0 to s-1
# t = a * b
C := 0
for j=0 to s-1
(C, S) := t[j] + a[j]*b[i] + C
t[j] := S
(C, S) := t[s] + C
t[s] := S
t[s+1] := C
# t = (t + m * N) / W
# W = 2^w
m := t[0] * n'[0] mod W
(C, S) := t[0] + m*n[0]
for j=1 to s-1
(C, S) := t[j] + m*n[j] + C
t[j-1] := S
(C, S) := t[s] + C
t[s-1] := S
t[s] := t[s+1] + C
# return t or t - N
# save as above, omitted
这样就得到了最终的 Coarse Integrated Operand Scanning 算法,下面是一段用 Rust 语言编写的实现:
// https://github.com/jiegec/rust-monty-comparison/blob/6c941d5c95d37dd9ee8f12aa57df577e0f2b623b/src/lib.rs#L89-L139
let mut res = [0u32; WORDS + 2];
// for i=0 to s-1
for i in 0..WORDS {
// C := 0
let mut c = 0;
// for j = 0 to s-1
for j in 0..WORDS {
// (C, S) := t[j] + a[j] * b[i] + C
let mut cs = res[j] as u64;
cs += self.num[j] as u64 * other.num[i] as u64;
cs += c as u64;
c = (cs >> 32) as u32;
// t[j] := S
res[j] = cs as u32;
}
// (C, S) := t[s] + C
let cs = res[WORDS] as u64 + c as u64;
// t[s] := S
res[WORDS] = cs as u32;
// t[s+1] := C
res[WORDS + 1] = (cs >> 32) as u32;
// m := t[0]*n'[0] mod W
let m: u32 = (res[0] as u64 * N_INV as u64) as u32;
// (C, S) := t[0] + m*n[0]
let mut cs = res[0] as u64 + m as u64 * N[0] as u64;
c = (cs >> 32) as u32;
// for j=1 to s-1
for j in 1..WORDS {
// (C, S) := t[j] + m*n[j] + C
cs = res[j] as u64;
cs += m as u64 * N[j] as u64;
cs += c as u64;
c = (cs >> 32) as u32;
// t[j-1] := S
res[j - 1] = cs as u32;
}
// (C, S) := t[s] + C
cs = res[WORDS] as u64 + c as u64;
// t[s-1] := S
res[WORDS - 1] = cs as u32;
// t[s] := t[s+1] + C
res[WORDS] = res[WORDS + 1] + (cs >> 32) as u32;
}
let res_scalar = MontyBigNum::from_u32_slice(&res[0..WORDS]);
let mut res_scalar_sub = res_scalar;
let borrow = bignum_sub(&mut res_scalar_sub, &MODULO);
if res[WORDS] != 0 || borrow == 0 {
res_scalar_sub
} else {
res_scalar
}
OpenSSL 也在函数 bn_mul_mont 中实现了这个算法。
常数时间¶
在 Montgomery 模乘的最后一步,需要把计算结果和
既然条件分支是为了和
此时为了让
也就是说,如果规定
另一种思路是,既然和
也就是说,结果会在
逆推¶
下面尝试逆推出 Montgomery 是如何设计 Montgomery 模乘算法的。
首先目标是求
于是下面的问题就来到了怎么找到
由于
于是
很幸运的是,前面已经推导过,当
所以两点观察很重要:一是换一个容易做除法的除数