From b7215a6b0c7af5e661ecc6b240b777395034c37c Mon Sep 17 00:00:00 2001 From: tuxxy Date: Thu, 21 Jun 2018 16:30:32 -0600 Subject: [PATCH 1/7] Build out BlockchainPower with unlock_account method --- nucypher/crypto/powers.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/nucypher/crypto/powers.py b/nucypher/crypto/powers.py index 3b31e045d..dc0cac08d 100644 --- a/nucypher/crypto/powers.py +++ b/nucypher/crypto/powers.py @@ -1,4 +1,5 @@ import inspect +import web3 from typing import List, Union from nucypher.keystore import keypairs @@ -61,6 +62,28 @@ class CryptoPowerUp(object): confers_public_key = False +class BlockchainPower(CryptoPowerUp): + """ + Allows for transacting on a Blockchain via web3 backend. + """ + + def __init__(self, account: str): + """ + Instantiates a BlockchainPower for the given account id. + """ + self.account = account + + def unlock_account(self, password: str, duration: int = None): + """ + Unlocks the account for the specified duration. If no duration is + provided, it will remain unlocked indefinitely. + """ + is_unlocked = web3.personal.unlockAccount(self.account, password, + duration=duration) + if not is_unlocked: + raise PowerUpError("Account failed to unlock for {}".format(self.account)) + + class KeyPairBasedPower(CryptoPowerUp): confers_public_key = True _keypair_class = keypairs.Keypair From 5ca7bb8e7e6db82b6b3438f535a31857726f353b Mon Sep 17 00:00:00 2001 From: tuxxy Date: Thu, 21 Jun 2018 17:19:58 -0600 Subject: [PATCH 2/7] Add sign_message method --- nucypher/crypto/powers.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/nucypher/crypto/powers.py b/nucypher/crypto/powers.py index dc0cac08d..41e1e41f8 100644 --- a/nucypher/crypto/powers.py +++ b/nucypher/crypto/powers.py @@ -6,6 +6,7 @@ from nucypher.keystore import keypairs from nucypher.keystore.keypairs import SigningKeypair, EncryptingKeypair from umbral.keys import UmbralPublicKey, UmbralPrivateKey, UmbralKeyingMaterial from umbral import pre +from web3.eth import Eth class PowerUpError(TypeError): @@ -78,11 +79,21 @@ class BlockchainPower(CryptoPowerUp): Unlocks the account for the specified duration. If no duration is provided, it will remain unlocked indefinitely. """ - is_unlocked = web3.personal.unlockAccount(self.account, password, - duration=duration) - if not is_unlocked: + self.is_unlocked = web3.personal.unlockAccount(self.account, password, + duration=duration) + if not self.is_unlocked: raise PowerUpError("Account failed to unlock for {}".format(self.account)) + def sign_message(self, message: bytes): + """ + Signs the message with the private key of the BlockchainPower. + """ + if not self.is_unlocked: + raise PowerUpError("Account is not unlocked.") + + signature = Eth.sign(self.account, data=message) + return signature + class KeyPairBasedPower(CryptoPowerUp): confers_public_key = True From 91314b45242923a54f08f14e9570ad144849f5ed Mon Sep 17 00:00:00 2001 From: tuxxy Date: Tue, 26 Jun 2018 01:57:51 -0600 Subject: [PATCH 3/7] Add sign and verify methods to the ControlCircumflex --- nucypher/blockchain/eth/interfaces.py | 40 ++++++++++++++++++++++++--- nucypher/characters.py | 1 + nucypher/crypto/powers.py | 10 ++++--- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/nucypher/blockchain/eth/interfaces.py b/nucypher/blockchain/eth/interfaces.py index b7843816a..6831613cf 100644 --- a/nucypher/blockchain/eth/interfaces.py +++ b/nucypher/blockchain/eth/interfaces.py @@ -1,3 +1,4 @@ +import binascii import json import os import warnings @@ -5,6 +6,9 @@ from pathlib import Path from typing import Tuple, List from constant_sorrow import constants +from eth_utils import to_canonical_address +from eth_keys.datatypes import PublicKey, Signature +from web3.providers.eth_tester.main import EthereumTesterProvider from web3 import Web3, WebsocketProvider, HTTPProvider, IPCProvider from web3.contract import Contract @@ -244,13 +248,13 @@ class ControlCircumflex: # # If custom __providers are not injected... - self.__providers = list() if providers is None else providers + self._providers = list() if providers is None else providers if providers is None: - # Mutates self.__providers + # Mutates self._providers self.add_provider(endpoint_uri=endpoint_uri, websocket=websocket, ipc_path=ipc_path, timeout=timeout) - web3_instance = Web3(providers=self.__providers) # Instantiate Web3 object with provider + web3_instance = Web3(providers=self._providers) # Instantiate Web3 object with provider self.w3 = web3_instance # capture web3 # if a SolidityCompiler class instance was passed, compile from solidity source code @@ -308,7 +312,7 @@ class ControlCircumflex: else: raise self.InterfaceError("Invalid interface parameters. Pass endpoint_uri or ipc_path") - self.__providers.append(provider) + self._providers.append(provider) def get_contract_factory(self, contract_name) -> Contract: """Retrieve compiled interface data from the cache and return web3 contract""" @@ -390,3 +394,31 @@ class DeployerCircumflex(ControlCircumflex): contract_abi=contract_factory.abi) return contract, txhash + + def call_backend_sign(self, account: str, message: bytes) -> str: + """ + Calls the appropriate signing function for the specified account on the + backend. If the backend is based on eth-tester, then it uses the + eth-tester signing interface to do so. + """ + provider = self._providers[0] + if isinstance(provider, EthereumTesterProvider): + address = to_canonical_address(account) + sig_key = provider.ethereum_tester.backend._key_lookup[address] + signed_message = sig_key.sign_msg(message) + return signed_message.to_hex() + else: + return self.w3.eth.sign(account, data=message) # Technically deprecated... + + def call_backend_verify(self, pubkey: bytes , signature: str, msg_hash: bytes): + """ + Verifies a hex string signature and message hash are from the provided + public key. + """ + eth_sig = Signature(signature_bytes=unhexlify(signature[2:])) + eth_pubkey = PublicKey(public_key_bytes=pubkey) + + is_valid_sig = eth_sig.verify_msg_hash(msg_hash, eth_pubkey) + sig_pubkey = eth_sig.recovery_public_key_from_msg_hash(msg_hash) + + return is_valid_sig and (sig_pubkey == eth_pubkey) diff --git a/nucypher/characters.py b/nucypher/characters.py index 7407e98b3..2582f0876 100644 --- a/nucypher/characters.py +++ b/nucypher/characters.py @@ -464,6 +464,7 @@ class Alice(Character, PolicyAuthor): _default_crypto_powerups = [SigningPower, EncryptingPower, DelegatingPower] def __init__(self, is_me=True, federated_only=False, *args, **kwargs): + Character.__init__(self, is_me=is_me, federated_only=federated_only, *args, **kwargs) if is_me and not federated_only: # TODO: 289 PolicyAuthor.__init__(self, *args, **kwargs) diff --git a/nucypher/crypto/powers.py b/nucypher/crypto/powers.py index 41e1e41f8..78bc8cbfa 100644 --- a/nucypher/crypto/powers.py +++ b/nucypher/crypto/powers.py @@ -6,7 +6,6 @@ from nucypher.keystore import keypairs from nucypher.keystore.keypairs import SigningKeypair, EncryptingKeypair from umbral.keys import UmbralPublicKey, UmbralPrivateKey, UmbralKeyingMaterial from umbral import pre -from web3.eth import Eth class PowerUpError(TypeError): @@ -68,19 +67,22 @@ class BlockchainPower(CryptoPowerUp): Allows for transacting on a Blockchain via web3 backend. """ - def __init__(self, account: str): + def __init__(self, blockchain: 'Blockchain', account: str): """ Instantiates a BlockchainPower for the given account id. """ + self.blockchain = blockchain self.account = account + self.is_unlocked = False def unlock_account(self, password: str, duration: int = None): """ Unlocks the account for the specified duration. If no duration is provided, it will remain unlocked indefinitely. """ - self.is_unlocked = web3.personal.unlockAccount(self.account, password, - duration=duration) + self.is_unlocked = self.blockchain.interface.w3.personal.unlockAccount( + self.account, password, duration=duration) + if not self.is_unlocked: raise PowerUpError("Account failed to unlock for {}".format(self.account)) From c2408eddce1f7ebba8114b61cc5f66561305ec6d Mon Sep 17 00:00:00 2001 From: tuxxy Date: Sat, 23 Jun 2018 16:03:02 -0600 Subject: [PATCH 4/7] Add __del__ to BlockchainPower --- nucypher/crypto/powers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nucypher/crypto/powers.py b/nucypher/crypto/powers.py index 78bc8cbfa..cbfcb2f6c 100644 --- a/nucypher/crypto/powers.py +++ b/nucypher/crypto/powers.py @@ -93,9 +93,15 @@ class BlockchainPower(CryptoPowerUp): if not self.is_unlocked: raise PowerUpError("Account is not unlocked.") - signature = Eth.sign(self.account, data=message) + signature = self.blockchain.interface.call_backend_sign(self.account, message) return signature + def __del__(self): + """ + Deletes the blockchain power and locks the account. + """ + self.blockchain.interface.w3.personal.lockAccount(self.account) + class KeyPairBasedPower(CryptoPowerUp): confers_public_key = True From 3a9d36202cbe456a49b7f4a38ac6bc5046e0b3cd Mon Sep 17 00:00:00 2001 From: tuxxy Date: Sun, 24 Jun 2018 15:46:07 -0600 Subject: [PATCH 5/7] Make call_backend_verify use eth_keys objects --- nucypher/blockchain/eth/interfaces.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/nucypher/blockchain/eth/interfaces.py b/nucypher/blockchain/eth/interfaces.py index 6831613cf..cea22b913 100644 --- a/nucypher/blockchain/eth/interfaces.py +++ b/nucypher/blockchain/eth/interfaces.py @@ -410,15 +410,12 @@ class DeployerCircumflex(ControlCircumflex): else: return self.w3.eth.sign(account, data=message) # Technically deprecated... - def call_backend_verify(self, pubkey: bytes , signature: str, msg_hash: bytes): + def call_backend_verify(self, pubkey: PublicKey, signature: Signature, msg_hash: bytes): """ Verifies a hex string signature and message hash are from the provided public key. """ - eth_sig = Signature(signature_bytes=unhexlify(signature[2:])) - eth_pubkey = PublicKey(public_key_bytes=pubkey) + is_valid_sig = signature.verify_msg_hash(msg_hash, pubkey) + sig_pubkey = signature.recover_public_key_from_msg_hash(msg_hash) - is_valid_sig = eth_sig.verify_msg_hash(msg_hash, eth_pubkey) - sig_pubkey = eth_sig.recovery_public_key_from_msg_hash(msg_hash) - - return is_valid_sig and (sig_pubkey == eth_pubkey) + return is_valid_sig and (sig_pubkey == pubkey) From 93303b9838d97d9a24393e5800a27ee22eaae4bc Mon Sep 17 00:00:00 2001 From: tuxxy Date: Sun, 24 Jun 2018 15:46:27 -0600 Subject: [PATCH 6/7] Add verify_message to BlockchainPower --- nucypher/crypto/powers.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/nucypher/crypto/powers.py b/nucypher/crypto/powers.py index cbfcb2f6c..80219f846 100644 --- a/nucypher/crypto/powers.py +++ b/nucypher/crypto/powers.py @@ -1,7 +1,10 @@ import inspect import web3 +from binascii import unhexlify +from eth_keys.datatypes import PublicKey, Signature from typing import List, Union +from eth_utils import keccak from nucypher.keystore import keypairs from nucypher.keystore.keypairs import SigningKeypair, EncryptingKeypair from umbral.keys import UmbralPublicKey, UmbralPrivateKey, UmbralKeyingMaterial @@ -96,6 +99,24 @@ class BlockchainPower(CryptoPowerUp): signature = self.blockchain.interface.call_backend_sign(self.account, message) return signature + def verify_message(self, address: str, pubkey: bytes, message: bytes, signature: str): + """ + Verifies that the message was signed by the keypair. + """ + # Check that address and pubkey match + eth_pubkey = PublicKey(pubkey) + if not eth_pubkey.to_checksum_address() == address: + raise ValueError("Pubkey address ({}) doesn't match the provided address ({})".format(eth_pubkey.to_checksum_address, address)) + + hashed_message = keccak(message) + eth_signature = Signature(signature_bytes=unhexlify(signature[2:])) + + if not self.blockchain.interface.call_backend_verify( + eth_pubkey, eth_signature, hashed_message): + raise PowerUpError("Signature is not valid for this message or pubkey.") + else: + return True + def __del__(self): """ Deletes the blockchain power and locks the account. From 9696821a98e5460f748690763171a9a59afe8658 Mon Sep 17 00:00:00 2001 From: tuxxy Date: Sun, 24 Jun 2018 15:46:48 -0600 Subject: [PATCH 7/7] Add test for BlockchainPower --- ...test_crypto_characters_and_their_powers.py | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/tests/characters/test_crypto_characters_and_their_powers.py b/tests/characters/test_crypto_characters_and_their_powers.py index 507ae1243..8373c267a 100644 --- a/tests/characters/test_crypto_characters_and_their_powers.py +++ b/tests/characters/test_crypto_characters_and_their_powers.py @@ -1,9 +1,11 @@ +import eth_utils import pytest from constant_sorrow import constants from nucypher.characters import Alice, Ursula, Character, Bob from nucypher.crypto import api -from nucypher.crypto.powers import CryptoPower, SigningPower, NoSigningPower +from nucypher.crypto.powers import CryptoPower, SigningPower, NoSigningPower,\ + BlockchainPower, PowerUpError """ Chapter 1: SIGNING @@ -72,6 +74,47 @@ def test_anybody_can_verify(mock_policy_agent): assert cleartext is constants.NO_DECRYPTION_PERFORMED +def test_character_blockchain_power(testerchain): + eth_address = testerchain.interface.w3.eth.accounts[0] + sig_privkey = testerchain.interface._providers[0].ethereum_tester.backend.\ + _key_lookup[eth_utils.to_canonical_address(eth_address)] + sig_pubkey = sig_privkey.public_key + + signer = Character(is_me=True) + signer._crypto_power.consume_power_up(BlockchainPower(testerchain, eth_address)) + + # Due to testing backend, the account is already unlocked. + power = signer._crypto_power.power_ups(BlockchainPower) + power.is_unlocked = True + #power.unlock_account('this-is-not-a-secure-password') + + data_to_sign = b'What does Ursula look like?!?' + sig = power.sign_message(data_to_sign) + + is_verified = power.verify_message(eth_address, sig_pubkey.to_bytes(), data_to_sign, sig) + assert is_verified == True + + # Test a bad message: + with pytest.raises(PowerUpError): + power.verify_message( eth_address, sig_pubkey.to_bytes(), data_to_sign + b'bad', sig) + + # Test a bad address/pubkey pair + with pytest.raises(ValueError): + power.verify_message( + testerchain.interface.w3.eth.accounts[1], + sig_pubkey.to_bytes(), + data_to_sign, + sig) + + # Test a signature without unlocking the account + power.is_unlocked = False + with pytest.raises(PowerUpError): + power.sign_message(b'test') + + # Test lockAccount call + del(power) + + """ Chapter 2: ENCRYPTION """