WOTS介绍
WOTS
全称为Winternitz one-time signature scheme
,翻译过来就被叫做Winternitz 一次性签名方案
,该数字签名为Robert Winternitz
提出的。
WOTS
这个数字签名是基于哈希的签名算法 ,该签名是Lamport
签名的改进算法,通过时间换空间的思路,减少了签名的体积。
应用场景:
作为XMSS
、SPHINCS
等现代化哈希签名体系的核心组件。
适合在抗练字密码学中作为安全的签名方案
由于WOTS
是一次性签名,通常配合Merkle
树扩展成多次使用的签名体系。
WOTS算法过程
对于这个算法来说,主要包括的还是密钥生成、签名、数字签名的验证三个算法,所以WOTS
算法过程主要就是分为密钥生成
、签名
、数字签名的验证
三个过程
注意:这个数字签名算法需要多看几遍才能看明白,而且第一次看的时候会有点懵,需要从整体把握这样才能明白每一步为什么要这样选取数据。
密钥生成
确定同时签名的位数 :首先需要先选取一个ω ≥ 2 \omega\ge2 ω ≥ 2 ,ω \omega ω 表示要同时签名的位数 ,并且还控制着公钥的哈希次数 。
这里例举一个例子帮助理解:比如:要签名a->01100001
,如果选取w = 2 w=2 w = 2 ,那么签名的时候对于a->01100001
就会将它的二进制位,按照俩个一组进行组合,将组合后的数据进行哈希计算h(01)、h(10)、h(00)、h(01)
。
注意:一般来说ω = 8 \omega=8 ω = 8 ,因为8
位是一个字节刚好可以一个字节同时进行数字签名。
选取一个哈希函数 :确定哈希函数的消息摘要长度是256
位,还是128
位将会影响后续公钥和私钥的长度。这边选取目前最常用的sha2-256
。即哈希摘要长度为256
位,转换为字节数就是32
字节。
私钥的选取 :要确定私钥的个数,私钥的大小。
私钥的个数由俩个部分组成:
私钥的生成的大小其实就是在x i ∈ [ 0 , 2 256 ) , i = 1 , 2 , . . , t − 1 x_i\in[0,2^{256}),i=1,2,..,t-1 x i ∈ [ 0 , 2 2 5 6 ) , i = 1 , 2 , . . , t − 1 这个范围内,而这个256
其实就是选取哈希函数消息摘要的位数。这样就形成了一个私钥。
S k = ( x t − 1 , . . . , x 1 , x 0 ) , t = t 1 + t 2 Sk=(x_{t-1},...,x_1,x_0),t=t_1+t_2
S k = ( x t − 1 , . . . , x 1 , x 0 ) , t = t 1 + t 2
公钥的计算 :公钥的计算其实比较简单,就是将私钥进行一定次数的哈希计算,形成哈希链。而哈希的次数是由ω \omega ω 决定的。
y i = h 2 ω − 1 ( x i ) = h 256 ( x i ) , 0 ≤ i ≤ t − 1 P k = ( y t − 1 , . . . , y 1 , y 0 ) y_i=h^{2^{\omega}-1}(x_i)=h^{256}(x_i),0\le i \le t-1\\
Pk=(y_{t-1},...,y_1,y_0)
y i = h 2 ω − 1 ( x i ) = h 2 5 6 ( x i ) , 0 ≤ i ≤ t − 1 P k = ( y t − 1 , . . . , y 1 , y 0 )
由于ω = 8 \omega=8 ω = 8 ,哈希的次数为255
,哈希的次数比较合适(太多会导致运算时间太高,太少导致容易被破解)。
签名
假设消息是m
,首先需要通过一次哈希,将消息m
映射到固定的长度,这里选取sha2-256
,所以消息m
会被映射成256
位。接下来就需要对这个256
位的哈希值进行分组操作,将这个256
位的哈希值分成ω \omega ω 位一组,这里也就是8
位一组。
h ( m ) = b t 1 − 1 ∣ ∣ . . . ∣ ∣ b 0 h(m) = b_{t_1-1}||...||b_0
h ( m ) = b t 1 − 1 ∣ ∣ . . . ∣ ∣ b 0
接下来需要对分组的b i b_i b i 计算一个校验和,具体的计算方式如下:
c = ∑ i = 0 t 1 − 1 ( 2 ω − b i ) c = \sum_{i=0}^{t_1-1}(2^{\omega}-b_i)
c = i = 0 ∑ t 1 − 1 ( 2 ω − b i )
求得校验和c
后,需要将c
转换为比特串,将转换成的比特串继续按照ω \omega ω 位一组进行分组,一共可以分得t 2 t_2 t 2 组,而这个t 2 t_2 t 2 就是前面私钥的个数t = t 1 + t 2 t=t_1+t_2 t = t 1 + t 2 中的t 2 t_2 t 2 ,而通过计算这里的t 2 = 2 t_2=2 t 2 = 2
c = b t 2 − 1 ∣ ∣ . . . ∣ ∣ b 0 c=b_{t_2-1}||...||b_0
c = b t 2 − 1 ∣ ∣ . . . ∣ ∣ b 0
d = c ∣ ∣ h ( m ) = d t − 1 ∣ ∣ . . . ∣ ∣ d t − t 2 − 1 ∣ ∣ . . . . d 0 d=c||h(m)=d_{t-1}||...||d_{t-t_2-1}||....d_0
d = c ∣ ∣ h ( m ) = d t − 1 ∣ ∣ . . . ∣ ∣ d t − t 2 − 1 ∣ ∣ . . . . d 0
最终就需要对消息的哈希值与校验和进行签名得到最终的签名V
,最坏的情况需要执行2 ω − 1 2^{\omega-1} 2 ω − 1 次的哈希函数运算:
v i = h d i ( s k i ) , i = 0 , 1 , . . . , t − 1 V = ( v t − 1 , . . . , t 1 , t 0 ) v_i =h^{d_i}(sk_i),i=0,1,...,t-1\\
V = (v_{t-1},...,t_1,t_0)
v i = h d i ( s k i ) , i = 0 , 1 , . . . , t − 1 V = ( v t − 1 , . . . , t 1 , t 0 )
验签算法
验证签名的运算其实就是将哈希链的计算次数达到255
次并判断是否与公钥相等。也就是先进行哈希函数的运算:
v i ′ = h 255 − d i ( v i ) V ′ = ( v t − 1 ′ , . . . , v 1 ′ , v 0 ′ ) v'_i = h^{255-d_i}(v_i)\\
V'=(v'_{t-1},...,v'_{1},v'_0)
v i ′ = h 2 5 5 − d i ( v i ) V ′ = ( v t − 1 ′ , . . . , v 1 ′ , v 0 ′ )
最后判断p k i pk_i p k i 是否与v i ′ v'_i v i ′ 相等,如果存在有一个数据不相等就说明签名验证不成功,说明消息或者某些数据被更改了。
注意:这是一次性数字签名方案,说明如果重复使用同一个私钥对不同消息进行数字签名,很可能就能伪造数字签名了。所以一次性数字签名在可能会存在重复使用私钥的情况 。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 from hashlib import sha256from Crypto.Util.number import long_to_bytesimport randomdef hash_chain (m,x ): t = m for i in range (x): t = sha256(t).digest() return t def key_gen (): omega,t1,t2 = 8 , 32 , 2 sk = [random.randint(0 ,2 **(omega * 32 )) for _ in range (t1+t2)] pk = [hash_chain(long_to_bytes(m),255 ) for m in sk] return sk,pk def sign (m,sk ): m_ = sha256(m).digest() V = [] for index, item in enumerate (m_): V.append(hash_chain(long_to_bytes(sk[index]),item)) sigma = long_to_bytes(sum (list (m_))) for index, item in enumerate (sigma): V.append(hash_chain(long_to_bytes(sk[index+32 ]),item)) return V def verify (m,V,pk ): m_ = sha256(m).digest() V_ = [] for index, item in enumerate (m_): V_.append(hash_chain(V[index],255 -item)) c = long_to_bytes(sum (list (m_))) for index,item in enumerate (c): V_.append(hash_chain(V[index+32 ],255 -item)) for i in range (len (V_)): if pk[i] != V_[i]: return False return True if __name__ == '__main__' : sk, pk = key_gen() m = b'AAAA' V = sign(m,sk) result = verify(m,V,pk) print (f"m没被修改={result} " ) result2 = verify(b'AAA' ,V,pk) print (f'm被修改过进行认证={result2} ' )
例题-nepctf2025_nepsign
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 from gmssl import sm3from random import SystemRandomfrom ast import literal_evalimport osflag = os.environ["FLAG" ] def SM3 (data ): d = [i for i in data] h = sm3.sm3_hash(d) return h def SM3_n (data, n=1 , bits=256 ): for _ in range (n): data = bytes .fromhex(SM3(data)) return data.hex ()[:bits // 4 ] class Nepsign (): def __init__ (self ): self.n = 256 self.hex_symbols = '0123456789abcdef' self.keygen() def keygen (self ): rng = SystemRandom() self.sk = [rng.randbytes(32 ) for _ in range (48 )] self.pk = [SM3_n(self.sk[_], 255 , self.n) for _ in range (48 )] return self.sk, self.pk def sign (self, msg, sk=None ): sk = sk if sk else self.sk m = SM3(msg) m_bin = bin (int (m, 16 ))[2 :].zfill(256 ) a = [int (m_bin[8 * i: 8 * i + 8 ], 2 ) for i in range (self.n // 8 )] step = [0 ] * 48 qq = [0 ] * 48 for i in range (32 ): step[i] = a[i] qq[i] = SM3_n(sk[i], step[i]) sum = [0 ] * 16 for i in range (16 ): sum [i] = 0 for j in range (1 , 65 ): if m[j - 1 ] == self.hex_symbols[i]: sum [i] += j step[i + 32 ] = sum [i] % 255 qq[i + 32 ] = SM3_n(sk[i + 32 ], step[i + 32 ]) return [i for i in qq] def verify (self, msg, qq, pk=None ): qq = [bytes .fromhex(i) for i in qq] pk = pk if pk else self.pk m = SM3(msg) m_bin = bin (int (m, 16 ))[2 :].zfill(256 ) a = [int (m_bin[8 * i: 8 * i + 8 ], 2 ) for i in range (self.n // 8 )] step = [0 ] * 48 pk_ = [0 ] * 48 for i in range (32 ): step[i] = a[i] pk_[i] = SM3_n(qq[i], 255 - step[i]) sum = [0 ] * 16 for i in range (16 ): sum [i] = 0 for j in range (1 , 65 ): if m[j - 1 ] == self.hex_symbols[i]: sum [i] += j step[i + 32 ] = sum [i] % 255 pk_[i + 32 ] = SM3_n(qq[i + 32 ], 255 - step[i + 32 ]) return True if pk_ == pk else False print ('initializing...' )Sign = Nepsign() while 1 : match int (input ('> ' )): case 1 : msg = bytes .fromhex(input ('msg: ' )) if msg != b'happy for NepCTF 2025' : print (Sign.sign(msg)) else : print ("You can't do that" ) case 2 : qq = literal_eval(input ('give me a qq: ' )) if Sign.verify(b'happy for NepCTF 2025' , qq): print (flag)
通过解读代码其实就发现本题其实可以得到,最关键的利用点就是要想办法获取消息b'happy for NepCTF 2025'
,签名过后的qq
数组。
其实使用题目中现成的函数,可以得到消息b'happy for NepCTF 2025'
的哈希值以及校验和的具体值,也就是上面代码中的step
的值,而step
的值代表着私钥哈希的次数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from pwn import *from gmssl import sm3import osfrom random import SystemRandomdef SM3 (data ): d = [i for i in data] h = sm3.sm3_hash(d) return h def SM3_n (data, n=1 , bits=256 ): for _ in range (n): data = bytes .fromhex(SM3(data)) return data.hex ()[:bits // 4 ] msg = b'happy for NepCTF 2025' def c_step (msg ): m = SM3_n(msg) m_bin = bin (int (m, 16 ))[2 :].zfill(256 ) step = [0 ]*48 a = [int (m_bin[8 * i: 8 * i + 8 ], 2 ) for i in range (256 // 8 )] hex_symbols = '0123456789abcdef' for i in range (32 ): step[i] = a[i] sum = [0 ] * 16 for i in range (16 ): sum [i] = 0 for j in range (1 , 65 ): if m[j - 1 ] == hex_symbols[i]: sum [i] += j step[i + 32 ] = sum [i] % 255 return step step_des = c_step(b'happy for NepCTF 2025' ) print (step_des)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 from pwn import *from gmssl import sm3import osfrom random import SystemRandomdef SM3 (data ): d = [i for i in data] h = sm3.sm3_hash(d) return h def SM3_n (data, n=1 , bits=256 ): for _ in range (n): data = bytes .fromhex(SM3(data)) return data.hex ()[:bits // 4 ] msg = b'happy for NepCTF 2025' def c_step (msg ): m = SM3_n(msg) m_bin = bin (int (m, 16 ))[2 :].zfill(256 ) step = [0 ]*48 a = [int (m_bin[8 * i: 8 * i + 8 ], 2 ) for i in range (256 // 8 )] hex_symbols = '0123456789abcdef' for i in range (32 ): step[i] = a[i] sum = [0 ] * 16 for i in range (16 ): sum [i] = 0 for j in range (1 , 65 ): if m[j - 1 ] == hex_symbols[i]: sum [i] += j step[i + 32 ] = sum [i] % 255 return step step_des = c_step(b'happy for NepCTF 2025' ) print (step_des)candidate = [] for i in range (48 ): while True : x = os.urandom(5 ) step = c_step(x) if step[i] <= step_des[i]: candidate.append(x.hex ()) break print (candidate)""" candate = ['e528ac746f', 'ad694be518', '12e5f725da', 'f38b0675a8', 'd126dd5379', 'e1bcffc062', 'f54fb14599', '9e4ce2c549', '42bb777a69', '37773f0771', '605fc06b20', '3dd0ba0a5b', 'f01e1de8f1', '651d6251d5', '07e90c3714', '2e70224c66', 'cd73d66284', 'dd270bb39b', '0afbbf91bb', '21954ad626', '53c6e4b606', 'cb6632070b', '25e43397f6', 'd6f54113c6', '0f1dbf213f', '8a71fbbcef', '1e484654d5', 'c0cce89280', '720c8fde3a', 'b42e82218b', '8fb6002d72', 'fa4804f1af', 'ff05854709', '0e83a61727', 'd087fccce3', '08958c158a', '5bc77069ea', 'b1173ddd72', 'f0bf6f2b4f', 'b144583404', '8ebc7c5eb8', 'dd82f6435d', '903377a152', 'f31923e8d1', '58ab6aac57', '6bc744f37c', '4ca8b192b1', '9f22cebc5f'] """
最后使用candidate
中的消息泄露消息b'happy for NepCTF 2025'
的qq
数组的值,最后再使用选项2
认证即可。最终exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 from pwn import *from gmssl import sm3import osfrom random import SystemRandomcontext.log_level = 'debug' def SM3 (data ): d = [i for i in data] h = sm3.sm3_hash(d) return h def SM3_n (data, n=1 , bits=256 ): for _ in range (n): data = bytes .fromhex(SM3(data)) return data.hex ()[:bits // 4 ] msg = b'happy for NepCTF 2025' def c_step (msg ): m = SM3_n(msg) m_bin = bin (int (m, 16 ))[2 :].zfill(256 ) step = [0 ]*48 a = [int (m_bin[8 * i: 8 * i + 8 ], 2 ) for i in range (256 // 8 )] hex_symbols = '0123456789abcdef' for i in range (32 ): step[i] = a[i] sum = [0 ] * 16 for i in range (16 ): sum [i] = 0 for j in range (1 , 65 ): if m[j - 1 ] == hex_symbols[i]: sum [i] += j step[i + 32 ] = sum [i] % 255 return step step_des = c_step(b'happy for NepCTF 2025' ) print (step_des)""" candate = [] for i in range(48): while True: x = os.urandom(5) step = c_step(x) if step[i] <= step_des[i]: candate.append(x.hex()) break print(candate) """ candate = ['e528ac746f' , 'ad694be518' , '12e5f725da' , 'f38b0675a8' , 'd126dd5379' , 'e1bcffc062' , 'f54fb14599' , '9e4ce2c549' , '42bb777a69' , '37773f0771' , '605fc06b20' , '3dd0ba0a5b' , 'f01e1de8f1' , '651d6251d5' , '07e90c3714' , '2e70224c66' , 'cd73d66284' , 'dd270bb39b' , '0afbbf91bb' , '21954ad626' , '53c6e4b606' , 'cb6632070b' , '25e43397f6' , 'd6f54113c6' , '0f1dbf213f' , '8a71fbbcef' , '1e484654d5' , 'c0cce89280' , '720c8fde3a' , 'b42e82218b' , '8fb6002d72' , 'fa4804f1af' , 'ff05854709' , '0e83a61727' , 'd087fccce3' , '08958c158a' , '5bc77069ea' , 'b1173ddd72' , 'f0bf6f2b4f' , 'b144583404' , '8ebc7c5eb8' , 'dd82f6435d' , '903377a152' , 'f31923e8d1' , '58ab6aac57' , '6bc744f37c' , '4ca8b192b1' , '9f22cebc5f' ] p = process(['python3.10' ,'server.py' ]) hash_list = [] qq = [] for i in range (len (candate)): p.sendlineafter(b'>' ,b'1' ) p.sendlineafter(b'msg' ,candate[i].encode()) hash = p.recvuntil(b']' ).decode()[3 :-1 ] hash = hash .split(',' ) hash_ = [] for j in range (len (hash )): if j == 0 : hash_.append(hash [j][1 :-1 ]) else : hash_.append(hash [j][2 :-1 ]) step = c_step(bytes .fromhex(candate[i])) t = step_des[i] - step[i] qq.append(SM3_n(bytes .fromhex(hash_[i]),t)) print (qq)p.sendlineafter(b'>' ,b'2' ) p.sendlineafter(b'give me a qq:' ,str (qq).encode()) p.interactive()