From 4433fc425a2a72905ef181a08bb69e5b4a13b360 Mon Sep 17 00:00:00 2001 From: ilyas585 Date: Tue, 11 Feb 2025 10:03:29 +0300 Subject: [PATCH] [#2] Add methods sign and verify --- .idea/.gitignore | 3 +++ frostfs_api/client/signer.py | 15 +++++++++++++++ frostfs_api/client/verifier.py | 18 ++++++++++++++++++ frostfs_api/crypto/key_extension.py | 14 ++++++++++++-- pytest.ini | 4 ++++ requirements.txt | 1 + tests/client/test_sign_verify.py | 29 +++++++++++++++++++++++++++++ tests/crypto/test_key_extension.py | 23 +++++++++++++++-------- tests/helpers/resources.py | 5 +++++ 9 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 frostfs_api/client/signer.py create mode 100644 frostfs_api/client/verifier.py create mode 100644 pytest.ini create mode 100644 tests/client/test_sign_verify.py create mode 100644 tests/helpers/resources.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/frostfs_api/client/signer.py b/frostfs_api/client/signer.py new file mode 100644 index 0000000..99947b8 --- /dev/null +++ b/frostfs_api/client/signer.py @@ -0,0 +1,15 @@ +import ecdsa +from hashlib import sha256 + + +class Signer: + def sign_rfc6979(self, private_key: bytes, message: bytes) -> bytes: + if len(private_key) == 0 or private_key is None: + raise ValueError(f"Incorrect private_key: {private_key}") + + # SECP256k1 is the Bitcoin elliptic curve + sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1, hashfunc=sha256) + + signature = sk.sign(message, sigencode=ecdsa.util.sigencode_string) + + return signature diff --git a/frostfs_api/client/verifier.py b/frostfs_api/client/verifier.py new file mode 100644 index 0000000..64cb148 --- /dev/null +++ b/frostfs_api/client/verifier.py @@ -0,0 +1,18 @@ +import ecdsa +from hashlib import sha256 + + +class Verifier: + def verify_rfc6979(self, public_key: bytes, message: bytes, signature: bytes) -> bool: + if len(public_key) == 0 or public_key is None: + raise ValueError(f"Incorrect public key: {public_key}") + + if message is None or signature is None: + return False + + vk = ecdsa.VerifyingKey.from_string(public_key, curve=ecdsa.SECP256k1, hashfunc=sha256) + + try: + return vk.verify(signature, message) + except ecdsa.BadSignatureError: + return False diff --git a/frostfs_api/crypto/key_extension.py b/frostfs_api/crypto/key_extension.py index 8f27946..50e1fe5 100644 --- a/frostfs_api/crypto/key_extension.py +++ b/frostfs_api/crypto/key_extension.py @@ -1,11 +1,21 @@ import base58 +import ecdsa class KeyExtension: - def get_private_key_from_wif(self, wif: str): + def get_private_key_from_wif(self, wif: str) -> bytes: + if len(wif) == 0 or wif is None: + raise ValueError(f"Empty WIF private key: {wif}") + decoded = base58.b58decode_check(wif) if len(decoded) != 34 or decoded[0] != 0x80 or decoded[-1] != 0x01: - raise ValueError("Incorrect WIF private key") + raise ValueError("Not decode WIF by base58") private_key = decoded[1:-1] return private_key + + def get_public_key(self, private_key: bytes): + if len(private_key) == 0 or private_key is None: + raise ValueError(f"Empty private key: {private_key}") + + return ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1).get_verifying_key().to_string() diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..b4e28b9 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] + +# tests markers +crypto: cryptographic functions tests diff --git a/requirements.txt b/requirements.txt index ee58f94..2d8c473 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ base58==2.1.1 ecdsa==0.19.0 + pytest==8.3.4 diff --git a/tests/client/test_sign_verify.py b/tests/client/test_sign_verify.py new file mode 100644 index 0000000..5b04eda --- /dev/null +++ b/tests/client/test_sign_verify.py @@ -0,0 +1,29 @@ +import ecdsa +import pytest + +from frostfs_api.client.signer import Signer +from frostfs_api.client.verifier import Verifier +from frostfs_api.crypto.key_extension import KeyExtension +from tests.helpers.resources import WIF + + +@pytest.mark.crypto +class TestSignAndVerify: + + @pytest.mark.parametrize("message", [b"Hello Frostfs API", b""]) + def test_sign_verify_success( + self, + client_signer: Signer, + client_verifier: Verifier, + client_key_extension: KeyExtension, + message: bytes + ): + private_key = client_key_extension.get_private_key_from_wif(WIF) + public_key = client_key_extension.get_public_key(private_key) + + signature = client_signer.sign_rfc6979(private_key, message) + assert client_verifier.verify_rfc6979(public_key, message, signature) is True + + def test_verify_rfc6979_empty_key(self, client_verifier: Verifier): + with pytest.raises(ValueError, match="Incorrect public key"): + client_verifier.verify_rfc6979(b'', b"Test message", b"signature") diff --git a/tests/crypto/test_key_extension.py b/tests/crypto/test_key_extension.py index 6fc1681..62f4e1c 100644 --- a/tests/crypto/test_key_extension.py +++ b/tests/crypto/test_key_extension.py @@ -1,16 +1,23 @@ +import pytest + from frostfs_api.crypto.key_extension import KeyExtension from tests.helpers.convert import convert_bytes_format_127_to_256 +from tests.helpers.resources import WIF, PRIVATE_KEY +@pytest.mark.crypto class TestKeyExtension: - WIF = "L1YS4myg3xHPvi3FHeLaEt7G8upwJaWL5YLV7huviuUtXFpzBMqZ" - PRIVATE_KEY = [ - -128, -5, 30, -36, -118, 85, -67, -6, 81, 43, 93, -38, 106, 21, -88, 127, 15, 125, -79, -17, -40, 77, -15, - 122, -88, 72, 109, -47, 125, -80, -40, -38 - ] - def test_get_private_key_from_wif_success(self, client_key_extension: KeyExtension): - private_key = client_key_extension.get_private_key_from_wif(self.WIF) + private_key = client_key_extension.get_private_key_from_wif(WIF) assert len(private_key) == 32, f"Not correct len of private key, expected: 32 Actual: {len(private_key)} " - assert private_key == bytes(convert_bytes_format_127_to_256(self.PRIVATE_KEY)), f"Not match private key, Expected: {self.PRIVATE_KEY}, Actual: {private_key}" + assert private_key == bytes(convert_bytes_format_127_to_256(PRIVATE_KEY)), \ + f"Not match private key, Expected: {PRIVATE_KEY}, Actual: {private_key}" + + def test_get_private_key_empty_wif(self, client_key_extension: KeyExtension): + with pytest.raises(ValueError, match="Empty WIF private"): + client_key_extension.get_private_key_from_wif("") + + def test_get_private_key_incorrect_wif(self, client_key_extension: KeyExtension): + with pytest.raises(ValueError, match="Invalid checksum"): + client_key_extension.get_private_key_from_wif("AAAAAAABBBBBBBBBBBCCCCCCCDDDDDDDDDDDDDDDDD") diff --git a/tests/helpers/resources.py b/tests/helpers/resources.py new file mode 100644 index 0000000..b642286 --- /dev/null +++ b/tests/helpers/resources.py @@ -0,0 +1,5 @@ +WIF = "L1YS4myg3xHPvi3FHeLaEt7G8upwJaWL5YLV7huviuUtXFpzBMqZ" +PRIVATE_KEY = [ + -128, -5, 30, -36, -118, 85, -67, -6, 81, 43, 93, -38, 106, 21, -88, 127, 15, 125, -79, -17, -40, 77, -15, + 122, -88, 72, 109, -47, 125, -80, -40, -38 + ] -- 2.47.2