ECDSA learning
前言
pwnhub六月内部赛出了一道ecdsa,由于当时还没学到签名的内容,再加上还要复习宪法学,下了个附件看了看题就扔一边了,现在有时间学习一下。
加密
首先,我们给出ECDSA处理过程的简要概述。本质上,包括四个基本要素:
1、参与数字签名的所有通信方都使用相同的全局域参数,用定义椭圆曲线以及曲线上的基点。
2、签名者首先需要生成一对公钥、私钥。对于私钥,签名者选择一个随机数或者伪随机数作为签名者的私钥。使用随机数和基点,签名者计算出椭圆曲线上的另一解点,作为签名者的公钥
3、对于待签名的消息计算其hash值。使用私钥、全局域参数、hash值来产生签名、签名包括两个整数,r和s
4、如果要对签名进行验证,验证者使用签名者的公钥、全局域参数、整数s作为输入,并将计算得到的输出值v与收到的r进行比较。如果v=r,则签名通过
随机选取一个在(1,n−1)区间上的整数dA作为私钥
其中n是选取椭圆曲线上的秩,也就是椭圆曲线加密方程的模数
Q=dA*G,其中Q是公钥,是由私钥生成的
G是椭圆曲线上的基点(有问题的话可以去看我之前那篇ecc加密原理的文章)
若对消息m进行签名,所采用过的椭圆曲线参数为D=(p,a,b,G,n,h),对应的密钥对为(k,Q),其中Q为公钥,k为私钥。
步骤如下:
1、生成一个临时密钥k
2、计算P=k*G,其中P是椭圆曲线上的一个点
3、取P点的横坐标x,使r≡x(mod n)
4、使用sha1函数计算message的hash值,使用e表示,e=H(m),注意这个哈希值需要转换为数值型
5、s≡k-1 * (e+ dA * r)(mod n)
输出值(r,s)即为数字签名
代码如下:
import ecdsa
gen = ecdsa.NIST256p.generator
order = gen.order()
# 生成私钥d_A
d_A = random.randrange(1,order-1)
# 生成公私钥对象
public_key = ecdsa.ecdsa.Public_key(gen,gen * d_A)
private_key = ecdsa.ecdsa.Private_key(public_key,d_A)
message = "Mu.Chen_want_girlfriends"
m = int(hashlib.sha1(message.encode("utf8")).hexdigest(),16)
# 临时密钥
k = random.randrange(1,order-1)
# 签名
signature = private_key.sign(m,k)
r = signature.r
s = signature.s
因为是dsa,所以解密又叫验证
步骤如下:
1、检查r和s是否为1到n-1之间的整数
2、使用sha,计算160位的hash值e
3、令w=inverse(s,n)
4、计算u1=ew u2=rw
5、计算解点X=(x1,y1)= u1 * G + u2 * Q
6、进行判断当v = = r vrvr时,校验成功
题目
回到这道ecdsa里,我们来具体看一下是如何解码校验
from ecdsa.ecdsa import *
import hashlib
import gmpy2
from secret import secrpt
def inverse_mod(a, m):
if a == 0:
return 0
return gmpy2.powmod(a, -1, m)
def bit_length(x):
return x.bit_length()
class RSZeroError(RuntimeError):
pass
class InvalidPointError(RuntimeError):
pass
class Signature(object):
"""ECDSA signature."""
def __init__(self, r, s):
self.r = r
self.s = s
class Public_key(object):
"""Public key for ECDSA."""
def __init__(self, generator, point, verify=True):
"""Low level ECDSA public key object.
:param generator: the Point that generates the group (the base point)
:param point: the Point that defines the public key
:param bool verify: if True check if point is valid point on curve
:raises InvalidPointError: if the point parameters are invalid or
point does not lay on the curve
"""
self.curve = generator.curve()
self.generator = generator
self.point = point
n = generator.order()
p = self.curve.p()
if not (0 <= point.x() < p) or not (0 <= point.y() < p):
raise InvalidPointError(
"The public point has x or y out of range."
)
if verify and not self.curve.contains_point(point.x(), point.y()):
raise InvalidPointError("Point does not lay on the curve")
if not n:
raise InvalidPointError("Generator point must have order.")
# for curve parameters with base point with cofactor 1, all points
# that are on the curve are scalar multiples of the base point, so
# verifying that is not necessary. See Section 3.2.2.1 of SEC 1 v2
if (
verify
and self.curve.cofactor() != 1
and not n * point == ellipticcurve.INFINITY
):
raise InvalidPointError("Generator point order is bad.")
class Private_key(object):
"""Private key for ECDSA."""
def __init__(self, public_key, secret_multiplier):
"""public_key is of class Public_key;
secret_multiplier is a large integer.
"""
self.public_key = public_key
self.secret_multiplier = secret_multiplier
def sign(self, hash, random_k):
"""Return a signature for the provided hash, using the provided
random nonce. It is absolutely vital that random_k be an unpredictable
number in the range [1, self.public_key.point.order()-1]. If
an attacker can guess random_k, he can compute our private key from a
single signature. Also, if an attacker knows a few high-order
bits (or a few low-order bits) of random_k, he can compute our private
key from many signatures. The generation of nonces with adequate
cryptographic strength is very difficult and far beyond the scope
of this comment.
May raise RuntimeError, in which case retrying with a new
random value k is in order.
"""
G = self.public_key.generator
n = G.order()
k = random_k % n
# Fix the bit-length of the random nonce,
# so that it doesn't leak via timing.
# This does not change that ks = k mod n
ks = k + n
kt = ks + n
if bit_length(ks) == bit_length(n):
p1 = kt * G
else:
p1 = ks * G
r = p1.x() % n
if r == 0:
raise RSZeroError("amazingly unlucky random number r")
s = (
inverse_mod(k, n)
* (hash + (self.secret_multiplier * r) % n)
) % n
if s == 0:
raise RSZeroError("amazingly unlucky random number s")
return Signature(r, s)
message = b'get the flag'
hash_message = int(hashlib.sha1(message).hexdigest(), 16)
public_key = Public_key(generator_192, generator_192 * secret)
private_key = Private_key(public_key, secret)
sig = private_key.sign(hash_message, secret)
flag = 'flag{' + hashlib.md5(str(secret).encode()).hexdigest() + '}'
print(sig.r, sig.s)
# 827738947342412163466256986352463260575568151152429823167 827738947342412163577310637036310636847677110838127448036
代码很长,但上面的都是关于ecdsa加密的实现代码,真正有用的只有下面的部分
message = b'get the flag'
hash_message = int(hashlib.sha1(message).hexdigest(), 16)
public_key = Public_key(generator_192, generator_192 * secret)
private_key = Private_key(public_key, secret)
sig = private_key.sign(hash_message, secret)
flag = 'flag{' + hashlib.md5(str(secret).encode()).hexdigest() + '}'
print(sig.r, sig.s)
# 827738947342412163466256986352463260575568151152429823167 827738947342412163577310637036310636847677110838127448036
已知message,s,r,公钥曲线,还知道公钥私钥所选用的是相同的临时密钥secret,并且此secret的md5值就是flag
那么思路就很明确了,就是利用hash_message,s,r在有限域n的环境下得到k(dA)。
n作为椭圆曲线的秩,直接在ecdsa库里用order声明一下就行。
用最后一条等式来写脚本就ok了
from ecdsa.ecdsa import *
import hashlib
import gmpy2
message = b'get the flag'
n=generator_192.order()
r=827738947342412163466256986352463260575568151152429823167
s=827738947342412163577310637036310636847677110838127448036
e = int(hashlib.sha1(message).hexdigest(), 16)
tmp=gmpy2.invert(s-r,n)
k=(e*tmp)%n
flag = 'flag{' + hashlib.md5(str(k).encode()).hexdigest() + '}'
print(flag)
官方wp的思路我没看懂,主要还是数学基础不牢,好多数学符号都还不认识,呜呜呜
总结
写完快5点了,准备去吃早饭了
通过这个题目,主要还是学到了dsa数字签名方面的内容,通过网上找到的加密脚本,更深入了解了代码构造,对题目中自己写解题脚本帮助还是巨大的。