diff --git a/setup.cfg b/setup.cfg index 48eb7f9b..e926e128 100644 --- a/setup.cfg +++ b/setup.cfg @@ -79,6 +79,8 @@ testing = black isort flake8 + substrate-interface + py-sr25519-bindings mqtt = aiomqtt<=0.1.3 certifi @@ -90,7 +92,8 @@ ethereum = # Required to fix a dependency issue with parsimonious and Python3.11 eth_abi==4.0.0b2; python_version>="3.11" polkadot = - substrate-interface==1.3.4 + substrate-interface + py-sr25519-bindings cosmos = cosmospy solana = diff --git a/src/aleph/sdk/chains/common.py b/src/aleph/sdk/chains/common.py index ddda3f04..e811ce3f 100644 --- a/src/aleph/sdk/chains/common.py +++ b/src/aleph/sdk/chains/common.py @@ -62,7 +62,6 @@ def _setup_sender(self, message: Dict) -> Dict: else: raise ValueError("Message sender does not match the account's public key.") - @abstractmethod async def sign_message(self, message: Dict) -> Dict: """ Returns a signed message from an Aleph message. @@ -71,6 +70,20 @@ async def sign_message(self, message: Dict) -> Dict: Returns: Dict: Signed message """ + message = self._setup_sender(message) + signature = await self.sign_raw(get_verification_buffer(message)) + message["signature"] = signature.hex() + return message + + @abstractmethod + async def sign_raw(self, buffer: bytes) -> bytes: + """ + Returns a signed message from a raw buffer. + Args: + buffer: Buffer to sign + Returns: + bytes: Signature in preferred format + """ raise NotImplementedError @abstractmethod @@ -143,3 +156,10 @@ def get_fallback_private_key(path: Optional[Path] = None) -> bytes: if not default_key_path.exists(): default_key_path.symlink_to(path) return private_key + + +def bytes_from_hex(hex_string: str) -> bytes: + if hex_string.startswith("0x"): + hex_string = hex_string[2:] + hex_string = bytes.fromhex(hex_string) + return hex_string diff --git a/src/aleph/sdk/chains/cosmos.py b/src/aleph/sdk/chains/cosmos.py index c1e29d5b..d6bba627 100644 --- a/src/aleph/sdk/chains/cosmos.py +++ b/src/aleph/sdk/chains/cosmos.py @@ -51,20 +51,12 @@ def __init__(self, private_key=None, hrp=DEFAULT_HRP): async def sign_message(self, message): message = self._setup_sender(message) - verif = get_verification_string(message) - - privkey = ecdsa.SigningKey.from_string(self.private_key, curve=ecdsa.SECP256k1) - signature_compact = privkey.sign_deterministic( - verif.encode("utf-8"), - hashfunc=hashlib.sha256, - sigencode=ecdsa.util.sigencode_string_canonize, - ) - signature_base64_str = base64.b64encode(signature_compact).decode("utf-8") base64_pubkey = base64.b64encode(self.get_public_key().encode()).decode("utf-8") + signature = await self.sign_raw(verif.encode("utf-8")) sig = { - "signature": signature_base64_str, + "signature": signature.decode("utf-8"), "pub_key": {"type": "tendermint/PubKeySecp256k1", "value": base64_pubkey}, "account_number": str(0), "sequence": str(0), @@ -72,7 +64,17 @@ async def sign_message(self, message): message["signature"] = json.dumps(sig) return message + async def sign_raw(self, buffer: bytes) -> bytes: + privkey = ecdsa.SigningKey.from_string(self.private_key, curve=ecdsa.SECP256k1) + signature_compact = privkey.sign_deterministic( + buffer, + hashfunc=hashlib.sha256, + sigencode=ecdsa.util.sigencode_string_canonize, + ) + return base64.b64encode(signature_compact) + def get_address(self) -> str: + # WARNING: Fails with OpenSSL >= 3.2.0 due to deprecation of ripemd160 return privkey_to_address(self.private_key) def get_public_key(self) -> str: diff --git a/src/aleph/sdk/chains/ethereum.py b/src/aleph/sdk/chains/ethereum.py index 5e3ff9f6..4f00cd7e 100644 --- a/src/aleph/sdk/chains/ethereum.py +++ b/src/aleph/sdk/chains/ethereum.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Dict, Optional, Union +from typing import Optional, Union from eth_account import Account from eth_account.messages import encode_defunct @@ -9,9 +9,9 @@ from ..exceptions import BadSignatureError from .common import ( BaseAccount, + bytes_from_hex, get_fallback_private_key, get_public_key, - get_verification_buffer, ) @@ -24,15 +24,11 @@ def __init__(self, private_key: bytes): self.private_key = private_key self._account = Account.from_key(self.private_key) - async def sign_message(self, message: Dict) -> Dict: - """Sign a message inplace.""" - message = self._setup_sender(message) - - msghash = encode_defunct(text=get_verification_buffer(message).decode("utf-8")) + async def sign_raw(self, buffer: bytes) -> bytes: + """Sign a raw buffer.""" + msghash = encode_defunct(text=buffer.decode("utf-8")) sig = self._account.sign_message(msghash) - - message["signature"] = sig["signature"].hex() - return message + return sig["signature"] def get_address(self) -> str: return self._account.address @@ -60,19 +56,14 @@ def verify_signature( BadSignatureError: If the signature is invalid. """ if isinstance(signature, str): - if signature.startswith("0x"): - signature = signature[2:] - signature = bytes.fromhex(signature) - else: - if signature.startswith(b"0x"): - signature = signature[2:] - signature = bytes.fromhex(signature.decode("utf-8")) + signature = bytes_from_hex(signature) if isinstance(public_key, bytes): public_key = "0x" + public_key.hex() if isinstance(message, bytes): - message = message.decode("utf-8") + message_hash = encode_defunct(primitive=message) + else: + message_hash = encode_defunct(text=message) - message_hash = encode_defunct(text=message) try: address = Account.recover_message(message_hash, signature=signature) if address.casefold() != public_key.casefold(): diff --git a/src/aleph/sdk/chains/nuls1.py b/src/aleph/sdk/chains/nuls1.py index c5629fb7..645a8fda 100644 --- a/src/aleph/sdk/chains/nuls1.py +++ b/src/aleph/sdk/chains/nuls1.py @@ -315,6 +315,10 @@ async def sign_message(self, message): message["signature"] = sig.serialize().hex() return message + async def sign_raw(self, buffer: bytes) -> bytes: + sig = NulsSignature.sign_data(self.private_key, buffer) + return sig.serialize() + def get_address(self): return address_from_hash( public_key_to_hash(self.get_public_key(), chain_id=self.chain_id) diff --git a/src/aleph/sdk/chains/nuls2.py b/src/aleph/sdk/chains/nuls2.py index ec1a367d..4ad65f19 100644 --- a/src/aleph/sdk/chains/nuls2.py +++ b/src/aleph/sdk/chains/nuls2.py @@ -1,5 +1,5 @@ import base64 -from typing import Union +from typing import Dict, Union from nuls2.model.data import ( NETWORKS, @@ -37,17 +37,23 @@ def __init__(self, private_key=None, chain_id=1, prefix=None): else: self.prefix = prefix - async def sign_message(self, message): - # sig = NulsSignature.sign_message(self.private_key, - # get_verification_buffer(message)) + async def sign_message(self, message: Dict) -> Dict: + """ + Returns a signed message from an Aleph message. + Args: + message: Message to sign + Returns: + Dict: Signed message + """ message = self._setup_sender(message) - - sig = sign_recoverable_message( - self.private_key, get_verification_buffer(message) - ) - message["signature"] = base64.b64encode(sig).decode() + signature = await self.sign_raw(get_verification_buffer(message)) + message["signature"] = signature.decode() return message + async def sign_raw(self, buffer: bytes) -> bytes: + sig = sign_recoverable_message(self.private_key, buffer) + return base64.b64encode(sig) + def get_address(self): return address_from_hash( public_key_to_hash(self.get_public_key(), chain_id=self.chain_id), diff --git a/src/aleph/sdk/chains/remote.py b/src/aleph/sdk/chains/remote.py index 8b27e615..b36036ac 100644 --- a/src/aleph/sdk/chains/remote.py +++ b/src/aleph/sdk/chains/remote.py @@ -77,6 +77,9 @@ async def sign_message(self, message: Dict) -> Dict: response.raise_for_status() return await response.json() + async def sign_raw(self, buffer: bytes) -> bytes: + raise NotImplementedError() + def get_address(self) -> str: return self._address diff --git a/src/aleph/sdk/chains/sol.py b/src/aleph/sdk/chains/sol.py index ca5ff736..ff870a4d 100644 --- a/src/aleph/sdk/chains/sol.py +++ b/src/aleph/sdk/chains/sol.py @@ -30,13 +30,19 @@ async def sign_message(self, message: Dict) -> Dict: """Sign a message inplace.""" message = self._setup_sender(message) verif = get_verification_buffer(message) + signature = await self.sign_raw(verif) sig = { "publicKey": self.get_address(), - "signature": encode(self._signing_key.sign(verif).signature), + "signature": encode(signature), } message["signature"] = json.dumps(sig) return message + async def sign_raw(self, buffer: bytes) -> bytes: + """Sign a raw buffer.""" + sig = self._signing_key.sign(buffer) + return sig.signature + def get_address(self) -> str: return encode(self._signing_key.verify_key) diff --git a/src/aleph/sdk/chains/substrate.py b/src/aleph/sdk/chains/substrate.py index cc73a8b0..13795568 100644 --- a/src/aleph/sdk/chains/substrate.py +++ b/src/aleph/sdk/chains/substrate.py @@ -1,47 +1,73 @@ import json -from typing import Union +import logging +from pathlib import Path +from typing import Optional, Union +from sr25519 import verify from substrateinterface import Keypair +from substrateinterface.utils.ss58 import ss58_decode from ..conf import settings -from .common import BaseAccount, get_verification_buffer +from ..exceptions import BadSignatureError +from .common import BaseAccount, bytes_from_hex, get_verification_buffer + +logger = logging.getLogger(__name__) class DOTAccount(BaseAccount): CHAIN = "DOT" CURVE = "sr25519" - def __init__(self, mnemonics=None, address_type=42): + def __init__(self, mnemonics: str, address_type=42): self.mnemonics = mnemonics self.address_type = address_type self._account = Keypair.create_from_mnemonic( - self.mnemonics, address_type=address_type + self.mnemonics, ss58_format=address_type ) async def sign_message(self, message): message = self._setup_sender(message) verif = get_verification_buffer(message).decode("utf-8") - sig = {"curve": self.CURVE, "data": self._account.sign(verif)} + signature = await self.sign_raw(verif.encode("utf-8")) + sig = {"curve": self.CURVE, "data": signature.hex()} message["signature"] = json.dumps(sig) return message - def get_address(self): + async def sign_raw(self, buffer: bytes) -> bytes: + return self._account.sign(buffer) + + def get_address(self) -> str: return self._account.ss58_address - def get_public_key(self): - return self._account.public_key + def get_public_key(self) -> str: + return "0x" + self._account.public_key.hex() -def get_fallback_account(): - return DOTAccount(mnemonics=get_fallback_mnemonics()) +def get_fallback_account(path: Optional[Path] = None) -> DOTAccount: + return DOTAccount(mnemonics=get_fallback_mnemonics(path)) -def get_fallback_mnemonics(): - try: - mnemonic = settings.PRIVATE_KEY_FILE.read_text() - except OSError: +def get_fallback_mnemonics(path: Optional[Path] = None) -> str: + path = path or settings.PRIVATE_MNEMONIC_FILE + if path.exists() and path.stat().st_size > 0: + mnemonic = path.read_text() + else: mnemonic = Keypair.generate_mnemonic() - settings.PRIVATE_KEY_FILE.write_text(mnemonic) + path.parent.mkdir(exist_ok=True, parents=True) + path.write_text(mnemonic) + default_mnemonic_path = path.parent / "default.mnemonic" + + # If the symlink exists but does not point to a file, delete it. + if ( + default_mnemonic_path.is_symlink() + and not default_mnemonic_path.resolve().exists() + ): + default_mnemonic_path.unlink() + logger.warning("The symlink to the mnemonic is broken") + + # Create a symlink to use this mnemonic by default + if not default_mnemonic_path.exists(): + default_mnemonic_path.symlink_to(path) return mnemonic @@ -50,6 +76,26 @@ def verify_signature( signature: Union[bytes, str], public_key: Union[bytes, str], message: Union[bytes, str], -) -> bool: - """TODO: Implement this""" - raise NotImplementedError("Not implemented yet") +) -> None: + if isinstance(signature, str): + signature = bytes_from_hex(signature) + if isinstance(public_key, str): + public_key = bytes_from_hex(public_key) + if isinstance(message, str): + message = message.encode() + + try: + # Another attempt with the data wrapped, as discussed in https://github.com/polkadot-js/extension/pull/743 + if not verify(signature, message, public_key) or verify( + signature, b"" + message + b"", public_key + ): + raise BadSignatureError + except Exception as e: + raise BadSignatureError from e + + +def verify_signature_with_ss58_address( + signature: Union[bytes, str], address: str, message: Union[bytes, str] +) -> None: + address_bytes = ss58_decode(address) + return verify_signature(signature, address_bytes, message) diff --git a/src/aleph/sdk/chains/tezos.py b/src/aleph/sdk/chains/tezos.py index af870c35..cffa3e78 100644 --- a/src/aleph/sdk/chains/tezos.py +++ b/src/aleph/sdk/chains/tezos.py @@ -25,14 +25,18 @@ async def sign_message(self, message: Dict) -> Dict: message = self._setup_sender(message) verif = get_verification_buffer(message) + signature = await self.sign_raw(verif) sig = { "publicKey": self.get_public_key(), - "signature": self._account.sign(verif), + "signature": signature.decode(), } message["signature"] = json.dumps(sig) return message + async def sign_raw(self, buffer: bytes) -> bytes: + return self._account.sign(buffer).encode() + def get_address(self) -> str: return self._account.public_key_hash() diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 264c8c9f..784b4ade 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -16,6 +16,11 @@ class Settings(BaseSettings): description="Path to the private key used to sign messages", ) + PRIVATE_MNEMONIC_FILE: Path = Field( + default=Path("substrate.mnemonic"), + description="Path to the mnemonic used to create Substrate keypairs", + ) + PRIVATE_KEY_STRING: Optional[str] = None API_HOST: str = "https://api2.aleph.im" MAX_INLINE_SIZE: int = 50000 @@ -57,3 +62,8 @@ class Config: settings.PRIVATE_KEY_FILE = Path( settings.CONFIG_HOME, "private-keys", "ethereum.key" ) + +if str(settings.PRIVATE_MNEMONIC_FILE) == "substrate.mnemonic": + settings.PRIVATE_MNEMONIC_FILE = Path( + settings.CONFIG_HOME, "private-keys", "substrate.mnemonic" + ) diff --git a/src/aleph/sdk/wallets/ledger/ethereum.py b/src/aleph/sdk/wallets/ledger/ethereum.py index 392a68d4..2ecdc5d3 100644 --- a/src/aleph/sdk/wallets/ledger/ethereum.py +++ b/src/aleph/sdk/wallets/ledger/ethereum.py @@ -9,7 +9,7 @@ from ledgereth.messages import sign_message from ledgereth.objects import LedgerAccount, SignedMessage -from ...chains.common import BaseAccount, get_verification_buffer +from ...chains.common import BaseAccount, bytes_from_hex, get_verification_buffer class LedgerETHAccount(BaseAccount): @@ -74,6 +74,12 @@ async def sign_message(self, message: Dict) -> Dict: message["signature"] = signature return message + async def sign_raw(self, buffer: bytes) -> bytes: + """Sign a raw buffer.""" + sig: SignedMessage = sign_message(buffer, dongle=self._device) + signature: HexStr = sig.signature + return bytes_from_hex(signature) + def get_address(self) -> str: return self._account.address diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 9952f847..4f62c0c5 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,3 +1,4 @@ +import json from pathlib import Path from tempfile import NamedTemporaryFile @@ -5,6 +6,7 @@ import aleph.sdk.chains.ethereum as ethereum import aleph.sdk.chains.sol as solana +import aleph.sdk.chains.substrate as substrate import aleph.sdk.chains.tezos as tezos from aleph.sdk.chains.common import get_fallback_private_key @@ -34,3 +36,17 @@ def tezos_account() -> tezos.TezosAccount: with NamedTemporaryFile(delete=False) as private_key_file: private_key_file.close() yield tezos.get_fallback_account(path=Path(private_key_file.name)) + + +@pytest.fixture +def substrate_account() -> substrate.DOTAccount: + with NamedTemporaryFile(delete=False) as private_key_file: + private_key_file.close() + yield substrate.get_fallback_account(path=Path(private_key_file.name)) + + +@pytest.fixture +def messages(): + messages_path = Path(__file__).parent / "messages.json" + with open(messages_path) as f: + return json.load(f) diff --git a/tests/unit/messages.json b/tests/unit/messages.json new file mode 100644 index 00000000..5b7931e1 --- /dev/null +++ b/tests/unit/messages.json @@ -0,0 +1,67 @@ +[ + { + "item_hash": "24717e60ba577b17c1f47796a4c4e3de887cbf99ce3276157fe7015857d1ba47", + "type": "POST", + "chain": "SOL", + "sender": "fishbsxxtW2iRwBgihKZEWGv4EMZ47G6ypx3P22Nhqx", + "signature": "{\"signature\":\"53uHLe8nGgHG5Pub2ZpGk2WSTNmspu8hoK7vRM1Lz1gBbaN5t6LLLDix585pJR3LpotinEMtYYrcyZsw4QfY2nf1\",\"publicKey\":\"fishbsxxtW2iRwBgihKZEWGv4EMZ47G6ypx3P22Nhqx\"}", + "item_type": "inline", + "item_content": "{\"type\":\"Permission\",\"address\":\"fishbsxxtW2iRwBgihKZEWGv4EMZ47G6ypx3P22Nhqx\",\"content\":{\"datasetID\":\"55998e1e21ac4d9fbd7612194fc175407320eb2e53c4e02719850a6ccfac4f26\",\"authorizer\":\"8zWzM8NVDrgqZMqcKhESsTumo1JTyhLjRDpBMZBCy394\",\"status\":\"GRANTED\",\"requestor\":\"FseBmSzpvpTTNiVyvTxSiVpyhQ5ChgViwCuvYNsfFqCS\",\"tags\":[\"FseBmSzpvpTTNiVyvTxSiVpyhQ5ChgViwCuvYNsfFqCS\",\"2JCJXePWQgBWZz4d7UuHFYhBwaPkuEhMrb4EEqf94qyu\"],\"executionCount\":0,\"maxExecutionCount\":1},\"time\":1693749978.337}", + "content": { + "time": 1693749978.337, + "type": "Permission", + "address": "fishbsxxtW2iRwBgihKZEWGv4EMZ47G6ypx3P22Nhqx", + "content": { + "tags": [ + "FseBmSzpvpTTNiVyvTxSiVpyhQ5ChgViwCuvYNsfFqCS", + "2JCJXePWQgBWZz4d7UuHFYhBwaPkuEhMrb4EEqf94qyu" + ], + "status": "GRANTED", + "datasetID": "55998e1e21ac4d9fbd7612194fc175407320eb2e53c4e02719850a6ccfac4f26", + "requestor": "FseBmSzpvpTTNiVyvTxSiVpyhQ5ChgViwCuvYNsfFqCS", + "authorizer": "8zWzM8NVDrgqZMqcKhESsTumo1JTyhLjRDpBMZBCy394", + "executionCount": 0, + "maxExecutionCount": 1 + } + }, + "time": 1693749978.337, + "channel": "FISHNET_DEMO_BERLIN_2", + "size": 472, + "confirmations": [ + { + "chain": "ETH", + "hash": "0x723b7bb333fd9a97ef0abe3a424ce5a61a64231a405f2aa5df0f8107506df8c2", + "height": 18056735 + } + ], + "confirmed": true + }, + { + "item_hash": "a17588da875f17a0775d7d80a87a2e1719edef401671d4a65936746c00a6a234", + "type": "AGGREGATE", + "chain": "ETH", + "sender": "0x51A58800b26AA1451aaA803d1746687cB88E0501", + "signature": "0x7a808edfecf2abac1606612ab7f9d52ff31eea7e2bc549824187eae9107a9c1a46ece07ec1cb28411d08394304c651b578658d2de4af30ca1a0526441dc543c11b", + "item_type": "inline", + "item_content": "{\"address\":\"0x51A58800b26AA1451aaA803d1746687cB88E0501\",\"key\":\"0x1ef1f1bb75314f8ef86039c96cf39f11754d9ad0binance\",\"content\":{\"1693923683606\":{\"version\":\"x25519-xsalsa20-poly1305\",\"nonce\":\"hX6Xt8UJbWMjDO9Q8oF1iboZYUu1DXzR\",\"ephemPublicKey\":\"8kvL6HEq3MefSfXBMjozkoQbLmHciRrkdKedIDXJsCA=\",\"ciphertext\":\"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\",\"sha256\":\"df4ba2c781428d5d92341f518895a57c697f1871ec672872e67a51c849ec5042\"}},\"time\":1693923684.147}", + "content": { + "key": "0x1ef1f1bb75314f8ef86039c96cf39f11754d9ad0binance", + "time": 1693923684.147, + "address": "0x51A58800b26AA1451aaA803d1746687cB88E0501", + "content": { + "1693923683606": { + "nonce": "hX6Xt8UJbWMjDO9Q8oF1iboZYUu1DXzR", + "sha256": "df4ba2c781428d5d92341f518895a57c697f1871ec672872e67a51c849ec5042", + "version": "x25519-xsalsa20-poly1305", + "ciphertext": "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", + "ephemPublicKey": "8kvL6HEq3MefSfXBMjozkoQbLmHciRrkdKedIDXJsCA=" + } + } + }, + "time": 1693923684.147, + "channel": "UNSLASHED", + "size": 4006, + "confirmations": [], + "confirmed": false + } +] \ No newline at end of file diff --git a/tests/unit/test_chain_ethereum.py b/tests/unit/test_chain_ethereum.py index 62e941ec..dea58c69 100644 --- a/tests/unit/test_chain_ethereum.py +++ b/tests/unit/test_chain_ethereum.py @@ -70,17 +70,25 @@ async def test_verify_signature(ethereum_account): get_verification_buffer(message), ) verify_signature( - bytes(message["signature"], "utf-8"), + message["signature"], bytes.fromhex(message["sender"][2:]), get_verification_buffer(message).decode("utf-8"), ) verify_signature( - bytes(message["signature"], "utf-8")[2:], + message["signature"], message["sender"], get_verification_buffer(message), ) +@pytest.mark.asyncio +async def test_verify_signature_with_processed_message(ethereum_account, messages): + message = messages[1] + verify_signature( + message["signature"], message["sender"], get_verification_buffer(message) + ) + + @pytest.mark.asyncio async def test_verify_signature_with_forged_signature(ethereum_account): account = ethereum_account @@ -138,3 +146,13 @@ async def test_verify_signature_wrong_public_key(ethereum_account): verify_signature( message["signature"], wrong_public_key, get_verification_buffer(message) ) + + +@pytest.mark.asyncio +async def test_sign_raw(ethereum_account): + buffer = b"SomeBuffer" + signature = await ethereum_account.sign_raw(buffer) + assert signature + assert isinstance(signature, bytes) + + verify_signature(signature, ethereum_account.get_address(), buffer) diff --git a/tests/unit/test_chain_solana.py b/tests/unit/test_chain_solana.py index 7384229f..5088158a 100644 --- a/tests/unit/test_chain_solana.py +++ b/tests/unit/test_chain_solana.py @@ -102,6 +102,13 @@ async def test_verify_signature(solana_account): ) +@pytest.mark.asyncio +async def test_verify_signature_with_processed_message(solana_account, messages): + message = messages[0] + signature = json.loads(message["signature"])["signature"] + verify_signature(signature, message["sender"], get_verification_buffer(message)) + + @pytest.mark.asyncio async def test_verify_signature_with_forged_signature(solana_account): message = asdict( @@ -119,3 +126,13 @@ async def test_verify_signature_with_forged_signature(solana_account): with pytest.raises(BadSignatureError): verify_signature(forged, message["sender"], get_verification_buffer(message)) + + +@pytest.mark.asyncio +async def test_sign_raw(solana_account): + buffer = b"SomeBuffer" + signature = await solana_account.sign_raw(buffer) + assert signature + assert isinstance(signature, bytes) + + verify_signature(signature, solana_account.get_address(), buffer) diff --git a/tests/unit/test_chain_substrate.py b/tests/unit/test_chain_substrate.py new file mode 100644 index 00000000..5e5778e3 --- /dev/null +++ b/tests/unit/test_chain_substrate.py @@ -0,0 +1,145 @@ +import json +from dataclasses import asdict, dataclass +from pathlib import Path +from tempfile import NamedTemporaryFile + +import pytest + +from aleph.sdk.chains.common import get_verification_buffer +from aleph.sdk.chains.substrate import ( + get_fallback_account, + verify_signature, + verify_signature_with_ss58_address, +) +from aleph.sdk.exceptions import BadSignatureError + + +@dataclass +class Message: + chain: str + sender: str + type: str + item_hash: str + + +def test_get_fallback_account(): + with NamedTemporaryFile() as private_key_file: + account = get_fallback_account(path=Path(private_key_file.name)) + assert account.CHAIN == "DOT" + assert account.CURVE == "sr25519" + assert account._account.ss58_address + + +@pytest.mark.asyncio +async def test_DOTAccount(substrate_account): + account = substrate_account + + message = Message("DOT", account.get_address(), "SomeType", "ItemHash") + signed = await account.sign_message(asdict(message)) + assert signed["signature"] + assert len(signed["signature"]) == 160 + + address = account.get_address() + assert address + assert isinstance(address, str) + assert len(address) == 48 + + pubkey = account.get_public_key() + assert isinstance(pubkey, str) + assert len(pubkey) == 66 + + +@pytest.mark.asyncio +async def test_verify_signature(substrate_account): + account = substrate_account + + message = asdict( + Message( + "DOT", + account.get_address(), + "POST", + "SomeHash", + ) + ) + await account.sign_message(message) + assert message["signature"] + signature = json.loads(message["signature"])["data"] + + verify_signature_with_ss58_address( + signature, message["sender"], get_verification_buffer(message) + ) + + verify_signature_with_ss58_address( + signature, + message["sender"], + get_verification_buffer(message).decode(), + ) + + verify_signature( + signature, account.get_public_key(), get_verification_buffer(message) + ) + verify_signature( + signature, + bytes.fromhex(account.get_public_key()[2:]), + get_verification_buffer(message), + ) + + +@pytest.mark.asyncio +async def test_verify_signature_with_forged_signature(substrate_account): + account = substrate_account + + message = asdict( + Message( + "DOT", + account.get_address(), + "POST", + "SomeHash", + ) + ) + await account.sign_message(message) + assert message["signature"] + + forged_signature = "deadbeef" * 16 + + with pytest.raises(BadSignatureError): + verify_signature_with_ss58_address( + forged_signature, message["sender"], get_verification_buffer(message) + ) + + +@pytest.mark.asyncio +async def test_verify_signature_wrong_public_key(substrate_account): + account = substrate_account + + message = asdict( + Message( + "DOT", + account.get_address(), + "POST", + "SomeHash", + ) + ) + + await account.sign_message(message) + assert message["signature"] + sig = json.loads(message["signature"]) + + wrong_public_key: str = "0x" + "0" * 64 + with pytest.raises(BadSignatureError): + verify_signature( + sig["data"], wrong_public_key, get_verification_buffer(message) + ) + + +@pytest.mark.asyncio +async def test_sign_raw(substrate_account): + buffer = b"SomeBuffer" + signature = await substrate_account.sign_raw(buffer) + assert signature + assert isinstance(signature, bytes) + + verify_signature(signature, substrate_account.get_public_key(), buffer) + verify_signature_with_ss58_address( + signature, substrate_account.get_address(), buffer + ) diff --git a/tests/unit/test_chain_tezos.py b/tests/unit/test_chain_tezos.py index 15985972..0beaffc9 100644 --- a/tests/unit/test_chain_tezos.py +++ b/tests/unit/test_chain_tezos.py @@ -71,3 +71,13 @@ async def test_decrypt_secp256k1(tezos_account: TezosAccount): decrypted = await tezos_account.decrypt(encrypted) assert isinstance(decrypted, bytes) assert content == decrypted + + +@pytest.mark.asyncio +async def test_sign_raw(tezos_account): + buffer = b"SomeBuffer" + signature = await tezos_account.sign_raw(buffer) + assert signature + assert isinstance(signature, bytes) + + verify_signature(signature, tezos_account.get_public_key(), buffer)