Repository: pybitcash/bitcash (language: python)

Issue:
Unsupported hash type ripemd160
Tested on OpenSSL 3.0.2, Python 3.10, Linux Ubuntu 22.04 LTS.

Hashlib raises ValueError: unsupported hash type ripemd160, in ripemd160_sha256 function in crypto.py. Found the reason for the issue [here](https://stackoverflow.com/questions/72409563/unsupported-hash-type-ripemd160-with-hashlib-in-python).

>Hashlib uses OpenSSL for ripemd160 and apparently OpenSSL disabled some older crypto algos around version 3.0 in November 2021. All the functions are still there but require manual enabling. See [issue 16994 of OpenSSL github project](https://github.com/openssl/openssl/issues/16994) for details.

The stackoverflow answer details how to enable legacy crypto algorithms, but I think a default solution is needed to use ripemd160.



Discussion / hints:
How does this impact BitCash?
BitCash uses the hash160 (ripemd160_sha256) function to convert public keys to cashaddress and public key hashes for output locking script of P2PKH outputs. This affects the primary functionality of BitCash as a P2PKH wallet.

hash160 uses the hashlib of python, which uses the OpenSSL implementation of ripemd160 which no longer works by default (as tested on OpenSSL 3.0.2, Python 3.10, Linux Ubuntu 22.04 LTS).
I am unable to reproduce this.

Can you send a minimal reproducible example?
```python
from hashlib import new, sha256
new("ripemd160", sha256(b"a").digest()).digest()
```

I tested this on OpenSSL 3.0.2, Python 3.10.6.
Apparently [OpenSSL had already marked ripemd160 as legacy algorithm](https://github.com/openssl/openssl/issues/16994). Do check if you have already enabled legacy_sect as noted [here](https://stackoverflow.com/questions/72409563/unsupported-hash-type-ripemd160-with-hashlib-in-python), if you are on the same version of OpenSSL.

This is what my default openssl.cnf file looked like. (only showing relevant options)
>openssl_conf = openssl_init
>
>[openssl_init]
>providers = provider_sect
>
>[provider_sect]
>default = default_sect
>
>[default_sect]
>\# activate = 1

This led the above python code to give "ValueError: unsupported hash type ripemd160"

After when I enabled legacy algorithms, as given [here](https://stackoverflow.com/questions/72409563/unsupported-hash-type-ripemd160-with-hashlib-in-python), the ripemd160 code gave no errors.
> ```
> from hashlib import new, sha256
> new("ripemd160", sha256(b"a").digest()).digest()
> ```

This works on my environment (macOS, Python 3.10.6, LibreSSL 3.3.6).

```python
In [1]: from hashlib import new, sha256
   ...: new("ripemd160", sha256(b"a").digest()).digest()
Out[1]: b'\x99CU\x19\x9eQo\xf7lO\xa4\xaa\xb3\x937\xb9\xd8L\xf1+'
```
I can try to reproduce in a different environment.

What would be a way to solve this within BitCash? And how much work would that involve?
It seems its a OpenSSl only problem. Other bitcoin libraries have dealt with the same: https://github.com/bitcoin/bitcoin/issues/23710.

Most straightforward way would be to code python native ripemd160 code, as done [here](https://github.com/bitcoin/bitcoin/pull/23716)
Seems pretty straightforward:

```python
def ripemd160(data):
    """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.
    return b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state)
```

Would you be willing to submit a PR for that?

Fix patch:
diff --git a/bitcash/_ripemd160.py b/bitcash/_ripemd160.py
new file mode 100644
index 00000000..06801ef8
--- /dev/null
+++ b/bitcash/_ripemd160.py
@@ -0,0 +1,107 @@
+# 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.
+"""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
+
+
+def ripemd160(data):
+    """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.
+    return b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state)
diff --git a/bitcash/crypto.py b/bitcash/crypto.py
index 431635dd..65e2bd9f 100644
--- a/bitcash/crypto.py
+++ b/bitcash/crypto.py
@@ -1,4 +1,5 @@
 from hashlib import new, sha256 as _sha256
+from bitcash._ripemd160 import ripemd160
 
 from coincurve import PrivateKey as ECPrivateKey, PublicKey as ECPublicKey
 
@@ -16,7 +17,10 @@ def double_sha256_checksum(bytestr):
 
 
 def ripemd160_sha256(bytestr):
-    return new("ripemd160", sha256(bytestr)).digest()
+    try:
+        return new("ripemd160", sha256(bytestr)).digest()
+    except ValueError:
+        return ripemd160(sha256(bytestr))
 
 
 hash160 = ripemd160_sha256


Test patch:
diff --git a/tests/test_format.py b/tests/test_format.py
index 715a7e83..99eebac2 100644
--- a/tests/test_format.py
+++ b/tests/test_format.py
@@ -1,4 +1,6 @@
 import pytest
+from bitcash import crypto
+from _pytest.monkeypatch import MonkeyPatch
 from bitcash.exceptions import InvalidAddress
 
 from bitcash.format import (
@@ -215,6 +217,65 @@ def test_public_key_to_address_regtest_uncompressed(self):
         )
 
 
+def mock_hashlib(name, data):
+    """Force ValueError from hashlib.new to fallback to pythonic ripemd160"""
+    raise ValueError()
+
+
+class TestPublicKeyToAddress_pythonic_ripemd:
+    def setup_method(self):
+        self.monkeypatch = MonkeyPatch()
+
+    def test_public_key_to_address_compressed(self):
+        self.monkeypatch.setattr(crypto, 'new', mock_hashlib)
+        assert (
+            public_key_to_address(PUBLIC_KEY_COMPRESSED)
+            == BITCOIN_CASHADDRESS_COMPRESSED
+        )
+
+    def test_public_key_to_address_uncompressed(self):
+        self.monkeypatch.setattr(crypto, 'new', mock_hashlib)
+        assert public_key_to_address(PUBLIC_KEY_UNCOMPRESSED) == BITCOIN_CASHADDRESS
+
+    def test_public_key_to_address_incorrect_length(self):
+        self.monkeypatch.setattr(crypto, 'new', mock_hashlib)
+        with pytest.raises(ValueError):
+            public_key_to_address(PUBLIC_KEY_COMPRESSED[:-1])
+
+    def test_public_key_to_address_incorrect_version(self):
+        self.monkeypatch.setattr(crypto, 'new', mock_hashlib)
+        with pytest.raises(ValueError):
+            public_key_to_address(PUBLIC_KEY_COMPRESSED, "incorrect-version")
+
+    def test_public_key_to_address_test_compressed(self):
+        self.monkeypatch.setattr(crypto, 'new', mock_hashlib)
+        assert (
+            public_key_to_address(PUBLIC_KEY_COMPRESSED, version="test")
+            == BITCOIN_CASHADDRESS_TEST_COMPRESSED
+        )
+
+    def test_public_key_to_address_test_uncompressed(self):
+        self.monkeypatch.setattr(crypto, 'new', mock_hashlib)
+        assert (
+            public_key_to_address(PUBLIC_KEY_UNCOMPRESSED, version="test")
+            == BITCOIN_CASHADDRESS_TEST
+        )
+
+    def test_public_key_to_address_regtest_compressed(self):
+        self.monkeypatch.setattr(crypto, 'new', mock_hashlib)
+        assert (
+            public_key_to_address(PUBLIC_KEY_COMPRESSED, version="regtest")
+            == BITCOIN_CASHADDRESS_REGTEST_COMPRESSED
+        )
+
+    def test_public_key_to_address_regtest_uncompressed(self):
+        self.monkeypatch.setattr(crypto, 'new', mock_hashlib)
+        assert (
+            public_key_to_address(PUBLIC_KEY_UNCOMPRESSED, version="regtest")
+            == BITCOIN_CASHADDRESS_REGTEST
+        )
+
+
 class TestCoordsToPublicKey:
     def test_coords_to_public_key_compressed(self):
         assert coords_to_public_key(PUBLIC_KEY_X, PUBLIC_KEY_Y) == PUBLIC_KEY_COMPRESSED