From 2ff6739ba6e1d2a1fec200374d73ec60c5550605 Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Thu, 9 Dec 2021 14:57:27 +0500 Subject: [PATCH] Replace hashlib.ripemd160 with python-only implemenation The implementation is taken from Bitcoin Core's test framework Closes #64 --- bitcointx/core/_ripemd160.py | 134 ++++++++++++++++++++++++++++++ bitcointx/core/scripteval.py | 6 +- bitcointx/core/serialize.py | 5 +- bitcointx/core/sha256.py | 19 ++++- bitcointx/tests/test_ripemd160.py | 30 +++++++ 5 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 bitcointx/core/_ripemd160.py create mode 100644 bitcointx/tests/test_ripemd160.py diff --git a/bitcointx/core/_ripemd160.py b/bitcointx/core/_ripemd160.py new file mode 100644 index 00000000..e427f715 --- /dev/null +++ b/bitcointx/core/_ripemd160.py @@ -0,0 +1,134 @@ +# 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. +# +# Type annotations added by Dmitry Petukhov +# +# The code is not constant-time! This should NOT be used for working with +# secret data, such as, for example building a MAC (message authentication +# code), etc. + +""" +Pure Python RIPEMD160 implementation. +The code is taken from Bitcoin Core's test framework. +This is needed because the openssl has deprecated ripemd160 algorithm +and it might not be available from hashlib anymore. +Runtime performance will be slow, but the alternative is having a compiled +dependency, which is too heavy. + +IMPORTANT: code is not constant-time! This should NOT be used for working +with secret data, such as, for example building a MAC (message +authentication code), etc. +""" + +from typing import Tuple + +# 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: int, y: int, z: int, i: int) -> int: + """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: int, i: int) -> int: + """Rotate the bottom 32 bits of x left by i bits.""" + return ((x << i) | ((x & 0xffffffff) >> (32 - i))) & 0xffffffff + + +def compress(h0: int, h1: int, h2: int, h3: int, h4: int, block: bytes + ) -> Tuple[int, int, int, int, int]: + """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 + + +def ripemd160(data: bytes) -> bytes: + """ + Compute the RIPEMD-160 hash of data. + + The code is not constant-time! This should NOT be used for working with + secret data, such as, for example building a MAC (message authentication + code), etc. + """ + # 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. + return b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state) diff --git a/bitcointx/core/scripteval.py b/bitcointx/core/scripteval.py index 57dd23c5..bb093579 100644 --- a/bitcointx/core/scripteval.py +++ b/bitcointx/core/scripteval.py @@ -28,6 +28,7 @@ import bitcointx.core._bignum import bitcointx.core.key import bitcointx.core.serialize +import bitcointx.core._ripemd160 from bitcointx.util import ensure_isinstance @@ -1012,10 +1013,7 @@ def check_args(n: int) -> None: elif sop == OP_RIPEMD160: check_args(1) - - h = hashlib.new('ripemd160') - h.update(stack.pop()) - stack.append(h.digest()) + stack.append(bitcointx.core._ripemd160.ripemd160(stack.pop())) elif sop == OP_ROT: check_args(3) diff --git a/bitcointx/core/serialize.py b/bitcointx/core/serialize.py index 20b7661d..dadee66d 100644 --- a/bitcointx/core/serialize.py +++ b/bitcointx/core/serialize.py @@ -23,6 +23,7 @@ List, Tuple, Sequence, Union, TypeVar, Type, Generic, Any, Optional, cast ) from ..util import ensure_isinstance +from ._ripemd160 import ripemd160 from io import BytesIO @@ -48,9 +49,7 @@ def Hash(msg: Union[bytes, bytearray]) -> bytes: def Hash160(msg: Union[bytes, bytearray]) -> bytes: """RIPEME160(SHA256(msg)) -> bytes""" - h = hashlib.new('ripemd160') - h.update(hashlib.sha256(msg).digest()) - return h.digest() + return ripemd160(hashlib.sha256(msg).digest()) class SerializationError(Exception): diff --git a/bitcointx/core/sha256.py b/bitcointx/core/sha256.py index 65daaa86..e87550ca 100644 --- a/bitcointx/core/sha256.py +++ b/bitcointx/core/sha256.py @@ -13,9 +13,13 @@ # Original C++ code was Copyright (c) 2014-2017 The Bitcoin Core developers # Original C++ code was licensed under MIT software license. -# This is needed for midstate SHA256, that is not available -# from hashlib.sha256. Runtime performance will be slow, -# but oftentimes this is acceptable. +""" +This is needed for midstate SHA256, that is not available +from hashlib.sha256. Runtime performance will be slow, but oftentimes this +is acceptable. IMPORTANT: code is not constant-time! This should NOT be used +for working with # secret data, such as, for example building a MAC (message +authentication code), etc. +""" # pylama:ignore=E501 @@ -68,6 +72,15 @@ def ReadBE32(buf: bytes) -> int: class CSHA256(): + """ + This class provides access to SHA256 routines, with access to + SHA256 midstate (which is not available from hashlib.sha256) + + The code is not constant-time! This should NOT be used for working with + secret data, such as, for example building a MAC (message authentication + code), etc. + """ + __slots__ = ['s', 'buf', 'bytes_count'] buf: bytes diff --git a/bitcointx/tests/test_ripemd160.py b/bitcointx/tests/test_ripemd160.py new file mode 100644 index 00000000..0445c659 --- /dev/null +++ b/bitcointx/tests/test_ripemd160.py @@ -0,0 +1,30 @@ +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import unittest +from bitcointx.core._ripemd160 import ripemd160 + + +class Test_RIPEMD160(unittest.TestCase): + def test_ripemd160(self) -> None: + """RIPEMD-160 test vectors.""" + # See https://homes.esat.kuleuven.be/~bosselae/ripemd160.html + # + # The following code is Copyright (c) 2021 Pieter Wuille + # (taken from test/functional/test_framework/ripemd160.py + # in Bitcoin Core source) + for msg, hexout in [ + (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") + ]: + self.assertEqual(ripemd160(msg).hex(), hexout)