Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions bitcointx/core/_ripemd160.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,38 @@
Runtime performance will be slow, but the alternative is having a compiled
dependency, which is too heavy.

When ``hashlib`` exposes a working ``ripemd160`` implementation (typically
via the OpenSSL legacy provider) we transparently delegate to it, which is
orders of magnitude faster than the pure-Python compression loop below. The
pure-Python fallback remains the source of truth for correctness and is the
implementation that is exercised by the test vectors.

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.
(btw, you cannot expect constant-time behavior from python code at all)
"""

import hashlib
from typing import Tuple


def _hashlib_ripemd160_available() -> bool:
"""Probe whether hashlib can produce a working ripemd160 digest.

On OpenSSL 3 without the legacy provider, ``hashlib.new('ripemd160')``
raises at construction time, so this check both verifies presence and that
the algorithm is actually usable.
"""
try:
hashlib.new('ripemd160', b'').digest()
except (ValueError, AttributeError): # pragma: no cover - environment dependent
return False
return True


_USE_HASHLIB = _hashlib_ripemd160_available()

# Message schedule indexes for the left path.
ML = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
Expand Down Expand Up @@ -120,6 +144,18 @@ def ripemd160(data: bytes) -> bytes:
secret data, such as, for example building a MAC (message authentication
code), etc.
"""
if _USE_HASHLIB:
return hashlib.new('ripemd160', data).digest()
return _ripemd160_pure(data)


def _ripemd160_pure(data: bytes) -> bytes:
"""Pure-Python reference implementation of RIPEMD-160.

Kept as the verifiable source of truth and as the fallback used when
``hashlib`` does not provide a usable ripemd160 (for example, OpenSSL 3
without the legacy provider).
"""
# Initialize state.
state = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0)
# Process full 64-byte blocks in the input.
Expand Down
62 changes: 41 additions & 21 deletions bitcointx/tests/test_ripemd160.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,49 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

import unittest
from bitcointx.core._ripemd160 import ripemd160

from bitcointx.core._ripemd160 import _ripemd160_pure, ripemd160

# RIPEMD-160 test vectors.
# See https://homes.esat.kuleuven.be/~bosselae/ripemd160.html
#
# The following table is Copyright (c) 2021 Pieter Wuille
# (taken from test/functional/test_framework/ripemd160.py
# in Bitcoin Core source)
_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"),
]


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")
]:
"""RIPEMD-160 test vectors against the public API."""
for msg, hexout in _VECTORS:
self.assertEqual(ripemd160(msg).hex(), hexout)

def test_ripemd160_pure_matches_vectors(self) -> None:
"""The pure-Python fallback implementation matches the spec vectors.

This guarantees correctness of the fallback path even on systems
where ``hashlib`` provides RIPEMD-160 (and therefore the public
``ripemd160`` function delegates to OpenSSL).
"""
for msg, hexout in _VECTORS:
self.assertEqual(_ripemd160_pure(msg).hex(), hexout)

def test_public_and_pure_agree(self) -> None:
"""Public entry point and pure-Python fallback agree byte for byte."""
for msg, _hexout in _VECTORS:
self.assertEqual(ripemd160(msg), _ripemd160_pure(msg))