2021SC@SDUSC
secp256k1曲线Curve结构定义——两种运算类型的实现
在libsecp256k1比特币密码算法开源库(五)中介绍了签名验证、公钥恢复和数字签名的数理逻辑,在具体代码实现中对
a
P
+
b
G
aP + bG
aP+bG类型和
a
G
aG
aG类型(即标题中的两种运算类型)的运算进行了一定的提速。
在数理逻辑中曾经介绍过,签名验证和公钥恢复中有一步需要计算一个点
Q
Q
Q,计算过程为
Q
=
u
1
G
+
u
2
H
A
Q=u_1G+u_2H_A
Q=u1?G+u2?HA?,这其实就是一个
a
P
+
b
G
aP + bG
aP+bG类型的运算;在公钥恢复中还有一步
H
A
=
(
s
Q
?
z
G
)
r
?
1
H_A = (sQ - zG)r^{-1}
HA?=(sQ?zG)r?1,实际上就是计算
(
s
r
?
1
)
Q
?
(
z
r
?
1
)
G
(sr^{-1})Q - (zr^{-1})G
(sr?1)Q?(zr?1)G,显然也是
a
P
+
b
G
aP + bG
aP+bG类型的运算;在数字签名中有一步需要计算
P
=
k
G
P=kG
P=kG,这是一个
a
G
aG
aG类型的运算。在ECMultContext中使用了ecmult函数实现了
a
P
+
b
G
aP + bG
aP+bG的高效运算,提高了签名验证和公钥恢复的效率;在ECMultGenContext中使用了ecmult_gen函数实现了
a
G
aG
aG的高效运算。
由于代码中取的变量名和数学表达式中有些不一致,我会在代码片前介绍清除对应关系,由于在代码片中不能使用LaTex数学公式,可能变量脚标会看的比较混乱,建议比对代码片前的文字。
Scalar
Scalar为256位标量值,使用8个32位的数组元素进行表示。secp256k1中多处用到标量scalar,其中私钥和数字签名中使用的随机数
k
k
k等都是标量,此外在本文中还会见到很多运算的中间量也是scalar标量
pub struct Scalar(pub [u32; 8]);
ECMultContext
在ECMultContext中使用了一个ecmult函数,这个函数可以加快
a
P
+
b
G
aP + bG
aP+bG类型的运算的速度,其中a和b都是Scalar类型标量,P和G都是Jacobian坐标点。具体ecmult函数实现代码如下所示:
pub fn ecmult(&self, r: &mut Jacobian, a: &Jacobian, na: &Scalar, ng: &Scalar) {
let mut tmpa = Affine::default();
let mut pre_a: [Affine; ECMULT_TABLE_SIZE_A] = Default::default();
let mut z = Field::default();
let mut wnaf_na = [0i32; 256];
let mut wnaf_ng = [0i32; 256];
let bits_na = ecmult_wnaf(&mut wnaf_na, na, WINDOW_A);
let mut bits = bits_na;
odd_multiples_table_globalz_windowa(&mut pre_a, &mut z, a);
let bits_ng = ecmult_wnaf(&mut wnaf_ng, &ng, WINDOW_G);
if bits_ng > bits {
bits = bits_ng;
}
r.set_infinity();
for i in (0..bits).rev() {
let mut n;
*r = r.double_var(None);
n = wnaf_na[i as usize];
if i < bits_na && n != 0 {
table_get_ge(&mut tmpa, &pre_a, n, WINDOW_A);
*r = r.add_ge_var(&tmpa, None);
}
n = wnaf_ng[i as usize];
if i < bits_ng && n != 0 {
table_get_ge_storage(&mut tmpa, &self.pre_g, n, WINDOW_G);
*r = r.add_zinv_var(&tmpa, &z);
}
}
if !r.is_infinity() {
r.z *= &z;
}
}
在签名验证和公钥恢复中都有形如aP + bG的运算,既然实现了ecmult函数,就可以在ECMultContext中使用这个函数加速签名验证和公钥恢复中的相关运算步骤。
签名验证verify
下面是签名验证的数学步骤:
u
1
=
s
?
1
z
?
m
o
d
?
n
u
2
=
s
?
1
r
?
m
o
d
?
n
Q
=
u
1
G
+
u
2
H
A
r
Q
=
x
Q
?
m
o
d
?
n
验
证
r
Q
=
r
?
是
否
成
立
u_1=s^{-1}z\,mod\,n\\u_2=s^{-1}r\,mod\,n\\Q=u_1G+u_2H_A\\r_Q=x_Q\,mod\,n\\验证r_Q=r\,是否成立
u1?=s?1zmodnu2?=s?1rmodnQ=u1?G+u2?HA?rQ?=xQ?modn验证rQ?=r是否成立在下面的代码中, sigr表示数字签名的r部分,即数学表达式中的
r
r
r; sigs表示数字签名的s部分,即数学表达式中的
s
s
s; pubkey表示公钥
H
A
H_A
HA?; message表示信息摘要(即信息被SHA256哈希后的结果)
z
z
z;
只要使用ecmult函数,将相应参数
u
1
、
G
、
u
2
、
H
A
u_1、G、u_2、H_A
u1?、G、u2?、HA?传入即可得到点
Q
Q
Q,下面是相关代码实现:
impl ECMultContext {
pub fn verify_raw(
&self,
sigr: &Scalar,
sigs: &Scalar,
pubkey: &Affine,
message: &Scalar,
) -> bool {
let c;
let (sn, u1, u2): (Scalar, Scalar, Scalar);
if sigr.is_zero() || sigs.is_zero() {
return false;
}
sn = sigs.inv_var();
u1 = &sn * message;
u2 = &sn * sigr;
let mut pubkeyj: Jacobian = Jacobian::default();
pubkeyj.set_ge(pubkey);
let mut pr: Jacobian = Jacobian::default();
self.ecmult(&mut pr, &pubkeyj, &u2, &u1);
if pr.is_infinity() {
return false;
}
c = sigr.b32();
let mut xr: Field = Default::default();
let _ = xr.set_b32(&c);
if pr.eq_x_var(&xr) {
return true;
}
if xr >= P_MINUS_ORDER {
return false;
}
xr += ORDER_AS_FE;
if pr.eq_x_var(&xr) {
return true;
}
false
}
}
公钥恢复recover
公钥恢复原理证明之前已经写过了,这里直接写公钥恢复的步骤:
先将数字签名的
r
r
r部分代入到椭圆曲线方程中得到一个纵坐标,将
r
r
r作为横坐标,在方程中得到的纵坐标,组合成一个点
Q
Q
Q。
计算
H
A
=
(
s
Q
?
z
G
)
r
?
1
H_A = (sQ - zG)r^{-1}
HA?=(sQ?zG)r?1得到公钥。
在下面的代码中, rn表示r的逆元
r
?
1
r^{-1}
r?1; u1表示
z
r
?
1
zr^{-1}
zr?1; u2表示
s
r
?
1
sr^{-1}
sr?1;
得到u1和u2就可以和签名验证过程使用的ecmult一样,计算
H
A
=
(
s
r
?
1
)
Q
?
(
z
r
?
1
)
G
H_A =(sr^{-1})Q - (zr^{-1})G
HA?=(sr?1)Q?(zr?1)G,即
H
A
=
u
2
Q
?
u
1
G
H_A =u_2Q - u_1G
HA?=u2?Q?u1?G,把参数传入函数ecmult中即可。下面即为相关代码实现:
impl ECMultContext {
pub fn recover_raw(
&self,
sigr: &Scalar,
sigs: &Scalar,
rec_id: u8,
message: &Scalar,
) -> Result<Affine, Error> {
debug_assert!(rec_id < 4);
if sigr.is_zero() || sigs.is_zero() {
return Err(Error::InvalidSignature);
}
let brx = sigr.b32();
let mut fx = Field::default();
let overflow = fx.set_b32(&brx);
debug_assert!(overflow);
if rec_id & 2 > 0 {
if fx >= P_MINUS_ORDER {
return Err(Error::InvalidSignature);
}
fx += ORDER_AS_FE;
}
let mut x = Affine::default();
if !x.set_xo_var(&fx, rec_id & 1 > 0) {
return Err(Error::InvalidSignature);
}
let mut xj = Jacobian::default();
xj.set_ge(&x);
let rn = sigr.inv();
let mut u1 = &rn * message;
u1 = -u1;
let u2 = &rn * sigs;
let mut qj = Jacobian::default();
self.ecmult(&mut qj, &xj, &u2, &u1);
let mut pubkey = Affine::default();
pubkey.set_gej_var(&qj);
if pubkey.is_infinity() {
Err(Error::InvalidSignature)
} else {
Ok(pubkey)
}
}
}
ECMultGenContext
ECMultGenContext 中实现了
a
G
aG
aG类型的快速计算,实际上只有签名过程的
P
=
k
G
P=kG
P=kG使用了这种类型的运算,在代码中为
a
G
aG
aG类型的运算定义了一个ecmult_gen函数,实现了对
a
G
aG
aG类型的快速计算。在代码中r是Jacobian类型的坐标点,gn是scalar标量,具体ecmult_gen函数实现代码如下所示:
pub fn ecmult_gen(&self, r: &mut Jacobian, gn: &Scalar) {
let mut adds = AffineStorage::default();
*r = self.initial;
let mut gnb = gn + &self.blind;
let mut add = Affine::default();
add.infinity = false;
for j in 0..64 {
let mut bits = gnb.bits(j * 4, 4);
for i in 0..16 {
adds.cmov(&self.prec[j][i], i as u32 == bits);
}
add = adds.into();
*r = r.add_ge(&add);
#[allow(unused_assignments)]
{
bits = 0;
}
}
add.clear();
gnb.clear();
}
签名sign
签名的步骤:
P
=
k
G
r
=
x
P
?
m
o
d
?
n
s
=
k
?
1
(
z
+
r
d
A
)
?
m
o
d
?
n
P=kG\\r=x_P\, mod \, n\\s=k^{-1}(z+rd_A) \,mod \,n
P=kGr=xP?modns=k?1(z+rdA?)modn在下面的代码中, seckey表示私钥
d
A
d_A
dA?; nonce表示随机数
k
k
k; n表示
z
+
r
d
A
z+rd_A
z+rdA?; sigr表示数字签名的r部分,即数学表达式中的
r
r
r; sigs表示数字签名的s部分,即数学表达式中的
s
s
s
结合我给下面代码中的相应注释,很容易理解相关运算的实现。值得一提的是Rust可以说就是为极致追求安全性的场景准备的一个语言,它可以很方便地管理内存,及时将敏感信息在内存中抹掉。比如在数字签名中随机数
k
k
k值和私钥
d
A
d_A
dA?就是非常敏感的信息,在之前我也写过为什么
k
k
k值不能泄露,即
k
k
k值泄露就意味着私钥泄露,同时
k
k
k值也不能重复使用,重复使用的结果也是
k
k
k值泄露,从而导致私钥泄露。既然如此还是把
k
k
k值尽快释放掉为好。在这里Rust为这种敏感信息的内存释放提供了很大的方便。
impl ECMultGenContext {
pub fn sign_raw(
&self,
seckey: &Scalar,
message: &Scalar,
nonce: &Scalar,
) -> Result<(Scalar, Scalar, u8), Error> {
let mut rp = Jacobian::default();
self.ecmult_gen(&mut rp, nonce);
let mut r = Affine::default();
r.set_gej(&rp);
r.x.normalize();
r.y.normalize();
let b = r.x.b32();
let mut sigr = Scalar::default();
let overflow = bool::from(sigr.set_b32(&b));
debug_assert!(!sigr.is_zero());
debug_assert!(!overflow);
let mut recid = (if overflow { 2 } else { 0 }) | (if r.y.is_odd() { 1 } else { 0 });
let mut n = &sigr * seckey;
n += message;
let mut sigs = nonce.inv();
sigs *= &n;
n.clear();
rp.clear();
r.clear();
if sigs.is_zero() {
return Err(Error::InvalidMessage);
}
if sigs.is_high() {
sigs = -sigs;
recid ^= 1;
}
Ok((sigr, sigs, recid))
}
}
到本篇为止,曲线Curve结构定义已经介绍完毕,你可以参考本文和之前的两篇对曲线的结构有一定的了解。 libsecp256k1比特币密码算法开源库(七)——secp256k1曲线Curve结构定义之坐标转换 libsecp256k1比特币密码算法开源库(八)——secp256k1曲线Curve结构定义之域元素的运算与压缩存储 以及本文 libsecp256k1比特币密码算法开源库(九)——secp256k1曲线Curve结构定义之两种运算类型的实现
后面将陆续介绍信息哈希摘要生成、公钥、私钥、数字签名等内容。
|