Browse Source

add pure-Python RIPEMD-160 code and wrapper routine

- The RIPEMD-160 hash algorithm has been deprecated in OpenSSLv3 and is
  therefore missing in the Python hashlib of Ubuntu 22.04 (Jammy Jellyfish).

- The provided wrap_ripemd160() routine monkey patches hashlib to use a
  pure-Python RIPEMD-160 implementation (also provided in this patch) if
  necessary. The routine needs to be called just once per program invocation.

- The RIPEMD-160 implementation is courtesy of Bitcoin Core developer Pieter
  Wuille.
The MMGen Project 2 years ago
parent
commit
40d90b37b8
2 changed files with 189 additions and 0 deletions
  1. 173 0
      mmgen/contrib/ripemd160.py
  2. 16 0
      mmgen/util.py

+ 173 - 0
mmgen/contrib/ripemd160.py

@@ -0,0 +1,173 @@
+# Copyright (c) 2021 Pieter Wuille
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#
+# Source: https://github.com/bitcoin/bitcoin/blob/master/test/functional/test_framework/ripemd160.py
+# Ported to MMGen with the following changes:
+# - replace leading spaces with tabs
+# - reimplement as class with digest() and hexdigest() methods
+# - add custom test output display
+
+"""Test-only pure Python RIPEMD160 implementation."""
+
+# Message schedule indexes for the left path.
+ML = [
+	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+	7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
+	3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
+	1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
+	4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13
+]
+
+# Message schedule indexes for the right path.
+MR = [
+	5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
+	6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
+	15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
+	8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
+	12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11
+]
+
+# Rotation counts for the left path.
+RL = [
+	11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
+	7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
+	11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
+	11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
+	9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6
+]
+
+# Rotation counts for the right path.
+RR = [
+	8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
+	9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
+	9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
+	15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
+	8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11
+]
+
+# K constants for the left path.
+KL = [0, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e]
+
+# K constants for the right path.
+KR = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0]
+
+
+def fi(x, y, z, i):
+	"""The f1, f2, f3, f4, and f5 functions from the specification."""
+	if i == 0:
+		return x ^ y ^ z
+	elif i == 1:
+		return (x & y) | (~x & z)
+	elif i == 2:
+		return (x | ~y) ^ z
+	elif i == 3:
+		return (x & z) | (y & ~z)
+	elif i == 4:
+		return x ^ (y | ~z)
+	else:
+		assert False
+
+
+def rol(x, i):
+	"""Rotate the bottom 32 bits of x left by i bits."""
+	return ((x << i) | ((x & 0xffffffff) >> (32 - i))) & 0xffffffff
+
+
+def compress(h0, h1, h2, h3, h4, block):
+	"""Compress state (h0, h1, h2, h3, h4) with block."""
+	# Left path variables.
+	al, bl, cl, dl, el = h0, h1, h2, h3, h4
+	# Right path variables.
+	ar, br, cr, dr, er = h0, h1, h2, h3, h4
+	# Message variables.
+	x = [int.from_bytes(block[4*i:4*(i+1)], 'little') for i in range(16)]
+
+	# Iterate over the 80 rounds of the compression.
+	for j in range(80):
+		rnd = j >> 4
+		# Perform left side of the transformation.
+		al = rol(al + fi(bl, cl, dl, rnd) + x[ML[j]] + KL[rnd], RL[j]) + el
+		al, bl, cl, dl, el = el, al, bl, rol(cl, 10), dl
+		# Perform right side of the transformation.
+		ar = rol(ar + fi(br, cr, dr, 4 - rnd) + x[MR[j]] + KR[rnd], RR[j]) + er
+		ar, br, cr, dr, er = er, ar, br, rol(cr, 10), dr
+
+	# Compose old state, left transform, and right transform into new state.
+	return h1 + cl + dr, h2 + dl + er, h3 + el + ar, h4 + al + br, h0 + bl + cr
+
+class ripemd160:
+
+	def __init__(self,data=b''):
+		"""Compute the RIPEMD-160 hash of data."""
+		# Initialize state.
+		state = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0)
+		# Process full 64-byte blocks in the input.
+		for b in range(len(data) >> 6):
+			state = compress(*state, data[64*b:64*(b+1)])
+		# Construct final blocks (with padding and size).
+		pad = b"\x80" + b"\x00" * ((119 - len(data)) & 63)
+		fin = data[len(data) & ~63:] + pad + (8 * len(data)).to_bytes(8, 'little')
+		# Process final blocks.
+		for b in range(len(fin) >> 6):
+			state = compress(*state, fin[64*b:64*(b+1)])
+		# Produce output.
+		self.res = b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state)
+
+	def hexdigest(self):
+		return self.res.hex()
+
+	def digest(self):
+		return self.res
+
+	def update(self,*args,**kwargs):
+		raise NotImplementedError('update() method not implemented for pure-Python ripemd160')
+
+	def copy(self,*args,**kwargs):
+		raise NotImplementedError('copy() method not implemented for pure-Python ripemd160')
+
+if __name__ == '__main__':
+
+	# See https://homes.esat.kuleuven.be/~bosselae/ripemd160.html
+	vectors = [
+		(b"", "9c1185a5c5e9fc54612808977ee8f548b2258d31"),
+		(b"a", "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe"),
+		(b"abc", "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"),
+		(b"message digest", "5d0689ef49d2fae572b881b123a85ffa21595f36"),
+		(b"abcdefghijklmnopqrstuvwxyz",
+			"f71c27109c692c1b56bbdceb5b9d2865b3708dbc"),
+		(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+			"12a053384a9c0c88e405a06c27dcf49ada62eb2b"),
+		(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
+			"b0e20b6e3116640286ed3a87a5713079b21f5189"),
+		(b"1234567890" * 8, "9b752e45573d4b39f4dbd3323cab82bf63326bfb"),
+		(b"a" * 1000000, "52783243c1697bdbe16d37f97f68f08325dc1528")
+	]
+
+	def test_ripemd160():
+		import sys
+		verbose = [s for s in ('verbose','--verbose','-v') if s in sys.argv]
+		sys.stderr.write("Testing RIPEMD-160 test vectors...")
+		sys.stderr.flush()
+
+		fs = '{:<7} {:62}{:3} {}\n'
+		col1_w = 62
+		if verbose:
+			sys.stderr.write('\n' + fs.format('BYTES','INPUT','','OUTPUT'))
+
+		for msg,hexout in vectors:
+
+			a = ripemd160(msg).hexdigest()
+			b = hexout
+			assert a == b, f'{a} != {b}'
+
+			if verbose:
+				sys.stderr.write(fs.format(
+					len(msg),
+					msg.decode()[:col1_w],
+					"..." if len(msg) > col1_w else "",
+					hexout ))
+
+		sys.stderr.write('OK\n')
+
+	test_ripemd160()

+ 16 - 0
mmgen/util.py

@@ -689,3 +689,19 @@ def run_session(callback,backend=None):
 
 	import asyncio
 	return asyncio.run(do())
+
+def wrap_ripemd160(called=[]):
+	if not called:
+		try:
+			import hashlib
+			hashlib.new('ripemd160')
+		except ValueError:
+			def hashlib_new_wrapper(name,*args,**kwargs):
+				if name == 'ripemd160':
+					return ripemd160(*args,**kwargs)
+				else:
+					return hashlib_new(name,*args,**kwargs)
+			from .contrib.ripemd160 import ripemd160
+			hashlib_new = hashlib.new
+			hashlib.new = hashlib_new_wrapper
+		called.append(True)