ECDSA(Elliptic Curve Digital Signature Algorithm)是一种基于椭圆曲线密码学的数字签名算法。它用于确保数字数据的完整性和身份验证,通常在信息安全和加密通信中使用。在日常使用中,通常会使用一些函数库来实现完成这个算法的功能,但是有部分情况是需要自高度自定义ECDSA相关逻辑的,这里分享JavaScript语言在不借助第三方库的前提下纯手写的ECDSA算法代码,并对其实现原理进行解释。
这里我直接copy之前自己文章中对其的详细描述。
ECDSA(Elliptic Curve Digital Signature Algorithm) 是使用椭圆曲线密码(ECC)对数字签名算法(DSA)的模拟。
ECDSA安全性依赖于基于椭圆曲线的有限群上的离散对数难题。与基于RSA的数字签名和基于有限域离散对数的数字签名相比,在相同的安全强度条件下,ECDSA方案具有如下特点:
设 G F ( p ) GF(p) GF(p)为有限域, E E E是有限域上 G F ( p ) GF(p) GF(p)上的椭圆曲线。选择 E E E上一点 G ∈ E G\in E G∈E, G G G的阶为满足安全要求的素数 n n n,即 n G = O nG=O nG=O( O O O为无穷远点)。选择一个随机数 d d d, d ∈ [ 1 , n − 1 ] d \in [1, n-1] d∈[1,n−1],计算 Q Q Q,使得 Q = d G Q=dG Q=dG,那么公钥为 ( n , Q ) (n, Q) (n,Q),私钥为 ( d ) (d) (d)。
签名者 A l i c e Alice Alice对消息 m m m签名的过程如下:
签名接收者 B o b Bob Bob对消息 m m m签名 ( r , s ) (r,s) (r,s)的验证过程如下:
由于
Q = d G s ≡ ( e + r d ) k − 1 ( m o d n ) k G = ( x , y ) u ≡ s − 1 e ( m o d n ) v ≡ s − 1 r ( m o d n ) ( x 1 , y 1 ) = u G + v Q Q=dG\\ s \equiv (e+rd)k^{-1}(mod \ n)\\ kG=(x,y)\\ u \equiv s^{-1}e(mod \ n)\\ v \equiv s^{-1}r(mod \ n)\\ (x_{1},y_{1})=uG+vQ Q=dGs≡(e+rd)k−1(mod n)kG=(x,y)u≡s−1e(mod n)v≡s−1r(mod n)(x1,y1)=uG+vQ
则有:
k ≡ ( e + r d ) s − 1 ≡ s − 1 e + s − 1 ≡ u + v d ( m o d n ) ( x , y ) = k G = u G + v d G = u G + v Q = ( x 1 , y 1 ) r 1 = x 1 m o d n = x m o d n = r k \equiv (e+rd)s^{-1} \equiv s^{-1}e+s^{-1} \equiv u+vd (\ mod \ n)\\ (x,y)=kG=uG+vdG=uG+vQ=(x_{1},y_{1})\\ r_{1}=x_{1} \ mod \ n = x \ mod \ n=r k≡(e+rd)s−1≡s−1e+s−1≡u+vd( mod n)(x,y)=kG=uG+vdG=uG+vQ=(x1,y1)r1=x1 mod n=x mod n=r
从头开始手写的ECDSA算法毫无疑问在算法的效率上以及拓展性上要比封装好的函数库低不少,那么为什么还需要自己纯手写实现ECDSA的逻辑呢?如果存在以下需求,从头手写是一个好的选择。
当你想使用自己手写的ECDSA的时候,就必须接受不能将其作为一个广泛应用、高频调用的算法包,因为算法的效率会差不少。
我们回顾下上面算法的细节,发现在JavaScript中实现,主要有两个方面的问题需要解决:
对于第一个问题,自从ECMAScript 2020(ES11)引入BigInt类型以来,JavaScript原生支持大整数运算。BigInt类型用于表示任意精度的整数,可以执行标准的算术操作。不需要额外的库,但在一些老版本的浏览器中可能不受支持。
对于第二个问题,我们尝试推导下。
首先明确,椭圆曲线上加法的定义,假设有一根椭圆曲线 E : y 2 = x 3 + a x + b E: y^2=x^3+ax+b E:y2=x3+ax+b,其中, a a a 和 b b b 是曲线的参数, x x x 和 y y y 是曲线上的点坐标。在椭圆曲线上的加法操作定义如下:
从上可以看出,我们主要需要处理的情况就是找到两点所确定的直线和曲线的交点,对于这个交点,我们尝试建立方程进行解出。
设 P P P的坐标为 ( x 1 , y 1 ) (x_{1},y_{1}) (x1,y1), Q Q Q的坐标为 ( x 2 , y 2 ) (x_{2},y_{2}) (x2,y2),两点所确立的直线为: y − y 1 = k ( x − x 1 ) + b y-y_{1}=k(x-x_{1})+b y−y1=k(x−x1)+b, k = x 1 − x 2 y 1 − y 2 k= \frac{x_{1}-x{2}}{y_{1}-y_{2}} k=y1−y2x1−x2。
通过求导的方式计算出 E E E上某点的斜率:
F ( x ) = x 3 + a x + b − y 2 F x ′ = 3 x 2 + a F y ′ = − 2 y k = − F y ′ F x ′ = 3 x 2 + a 2 y F(x)=x^3+ax+b-y^2\\F_{x}'=3x^2+a\\F_{y}'=-2y\\k=-\frac{F_{y}'}{F_{x}'}=\frac{3x^2+a}{2y} F(x)=x3+ax+b−y2Fx′=3x2+aFy′=−2yk=−Fx′Fy′=2y3x2+a
联立上式子,可以解得直线 L L L与曲线 E E E的交点:
x 3 = k 2 − x 1 − x 2 y 3 = y 1 + k ( x 2 − x 1 ) x_{3}=k^2-x_{1}-x_{2}\\y_{3}=y_{1}+k(x_{2}-x_{1}) x3=k2−x1−x2y3=y1+k(x2−x1)
椭圆曲线上的乘法,只需采用快速幂的方式进行加法运算即可。
至此,用JS纯手写的两大核心问题都解决了,具体细节处理见代码。
// 椭圆曲线点定义
class Point{
constructor(x,y){
this.x = BigInt(x);
this.y = BigInt(y);
}
}
// secp256k1曲线
const secp_p = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F'); // 2n ** 256n - 2n ** 32n - 977n;
const secp_n = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141'); // 2n ** 256n - 432420386565659656852420866394968145599n;
const secp_a = BigInt(0);
const Gx = BigInt('0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798');
const Gy = BigInt('0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8');
const secp_G = new Point(Gx, Gy);
function get_inv(x, y, p){
return x === 1n ? 1n : get_inv(y % x, y, p) * (y - y / x) % p;
}
// 求模逆元
function modInverse(b,p){
return get_inv(b%p,p,p);
}
// 最大公约数
function get_gcd(x,y){
return y ? get_gcd(y,x%y):x;
}
// 在有限域下计算的求模
function mod(a, b) {
const result = a % b;
return result >= 0n ? result : b + result;
}
// 椭圆曲线点加法
function point_add(pa, pb, p){
// 零点相加
if(pa.x === 0n && pa.y === 0n || pb.x === 0n && pb.y === 0n){
return new Point(pa.x+pb.x, pa.y+pb.y);
}
// 对称点相加
if(pa.x === pb.x && pa.y !== pb.y){
return new Point(0, 0);
}
// 定义斜率k的分母和分子
let k, k_num, k_den;
// k的分子分母计算
if(pa.x === pb.x && pa.y === pb.y){
// 自己和自己相加 斜率为该点切线
k_num = 3n * pa.x * pa.x + secp_a;
k_den = 2n * pa.y;
} else {
// 两点确定斜率
k_num = pa.y - pb.y;
k_den = pa.x - pb.x;
}
// k符号记载
let neg = 0;
if(k_num * k_den < 0n){
neg = 1;
k_num = k_num > 0n ? k_num : -k_num;
k_den = k_den > 0n ? k_den : -k_den;
}
// k化简分子分母
let gcd = get_gcd(k_num, k_den);
k_num /= gcd;
k_den /= gcd;
// 分母不为1,计算逆元
if(k_den !== 1n){
k_den = modInverse(k_den, p);
}
k = k_num * k_den % p;
if(neg === 1) {
k = -k;
}
// 计算最终结果
let x3 = mod(k * k - pa.x - pb.x, p);
let y3 = mod(k * (pa.x - x3) - pa.y, p);
return new Point(x3, y3);
}
// 椭圆曲线点快速乘法
function point_mul(n, g, p){
n = BigInt(n)
let ans = new Point(0, 0);
while(n > 0n){
if(n & 1n){
ans = point_add(ans, g, p);
}
g = point_add(g, g, p);
n >>= 1n;
}
return ans;
}
export default {
// 由私钥生成公钥
generatePublicKey(privateKey) {
// 私钥类型校验
if(typeof privateKey != 'bigint') {
throw new Error("私钥不是BigInt类型");
}
const publicKey = point_mul(privateKey, secp_G, secp_p);
return publicKey;
},
ecSign(privateKey, msgHash, k) {
// k类型校验
if(typeof k != 'bigint') {
throw new Error("随机数k不是BigInt类型");
}
// k大小校验
if(k < 1n || k > secp_n - 1n) {
throw new Error("随机数k不符合要求");
}
// 私钥类型校验
if(typeof privateKey != 'bigint') {
throw new Error("私钥不是BigInt类型");
}
// msg校验
// if(typeof msg != 'string') {
// throw new Error("msg不是string类型");
// }
const d = privateKey;
//const e = keccak256Int(msg);
const e = BigInt("0x" + msgHash);
const kG = point_mul(k, secp_G, secp_p);
const r = kG.x;
var s = modInverse(k, secp_n) * (e + r * d) % secp_n;
if(r === 0n || s === 0n) {
throw new Error("随机数k不符合要求");
}
const v = (kG.y % 2n) ^ (s * 2n < secp_n ? 0n : 1n);
if(s * 2n >= secp_n) {
s = secp_n - s;
}
return [r, s, v];
},
ecVerify(publicKey, msgHash, sign) {
// msg校验
// if(typeof msg != 'string') {
// throw new Error("msg不是string类型");
// }
const r = sign[0];
const s = sign[1];
const Q = publicKey;
//const e = keccak256Int(msg);
const e = BigInt("0x" + msgHash);
const s_inv = modInverse(s, secp_n);
const u = s_inv * e % secp_n;
const v = s_inv * r % secp_n;
const uG = point_mul(u, secp_G, secp_p);
const vQ = point_mul(v, Q, secp_p);
const uG_add_vQ = point_add(uG, vQ, secp_p);
const r1 = uG_add_vQ.x;
return r === r1;
},
// 提取ECDSA随机数 s有两个可能,会产生两个可能的k
// privateKey msgHash 都是bigint sign = [r, s]
extractRandomK(privateKey, msgHash, sign) {
// 私钥类型校验
if(typeof privateKey != 'bigint') {
throw new Error("私钥不是BigInt类型");
}
// msg校验
// if(typeof msg != 'string') {
// throw new Error("msg不是string类型");
// }
function recoverK(s) {
const d = privateKey;
const r = sign[0];
//const s = secp_n - sign[1];
//const e = keccak256Int(msg);
const e = BigInt("0x" + msgHash);
const s_inv = modInverse(s, secp_n);
const k = s_inv * (e + r * d) % secp_n;
return k;
}
return [recoverK(sign[1]), recoverK(secp_n - sign[1])];
},
}
ATFWUS 2023-09-02