Skip to content

Commit

Permalink
Merge pull request #37 from janezpodhostnik/janez/user-message-signin…
Browse files Browse the repository at this point in the history
…g-verification

User message signing and verification
  • Loading branch information
janezpodhostnik authored Nov 22, 2021
2 parents 1b8211e + f30c92a commit 8b10f4c
Show file tree
Hide file tree
Showing 17 changed files with 885 additions and 293 deletions.
627 changes: 387 additions & 240 deletions docs/python_SDK_guide.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions examples/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import examples.generate_key
import examples.events_examples
import examples.transactions_examples
import examples.user_message_examples


logging_config = toml.load(Path(__file__).parent.joinpath("./logging.toml"))
Expand Down
12 changes: 6 additions & 6 deletions examples/account_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
ProposalKey,
create_account_template,
Tx,
ContractTemplates,
TransactionTemplates,
cadence,
SignAlgo,
HashAlgo,
Expand Down Expand Up @@ -102,7 +102,7 @@ async def run(self, ctx: Config):
contract_code = cadence.String(contract_source_hex)
transaction = (
Tx(
code=ContractTemplates.addAccountContractTemplate,
code=TransactionTemplates.addAccountContractTemplate,
reference_block_id=latest_block.id,
payer=account_address,
proposal_key=ProposalKey(
Expand Down Expand Up @@ -163,7 +163,7 @@ async def run(self, ctx: Config):
contract_code = cadence.String(contract_source_hex)
transaction = (
Tx(
code=ContractTemplates.addAccountContractTemplate,
code=TransactionTemplates.addAccountContractTemplate,
reference_block_id=latest_block.id,
payer=account_address,
proposal_key=ProposalKey(
Expand Down Expand Up @@ -202,7 +202,7 @@ async def run(self, ctx: Config):
# Update account contract with a transaction
transaction = (
Tx(
code=ContractTemplates.updateAccountContractTemplate,
code=TransactionTemplates.updateAccountContractTemplate,
reference_block_id=latest_block.id,
payer=account_address,
proposal_key=ProposalKey(
Expand Down Expand Up @@ -263,7 +263,7 @@ async def run(self, ctx: Config):
contract_code = cadence.String(contract_source_hex)
transaction = (
Tx(
code=ContractTemplates.addAccountContractTemplate,
code=TransactionTemplates.addAccountContractTemplate,
reference_block_id=latest_block.id,
payer=account_address,
proposal_key=ProposalKey(
Expand Down Expand Up @@ -292,7 +292,7 @@ async def run(self, ctx: Config):

transaction = (
Tx(
code=ContractTemplates.removeAccountContractTemplate,
code=TransactionTemplates.removeAccountContractTemplate,
reference_block_id=latest_block.id,
payer=account_address,
proposal_key=ProposalKey(
Expand Down
82 changes: 72 additions & 10 deletions examples/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,70 @@ async def random_account(
ctx: Config,
contracts: dict[Annotated[str, "name"], Annotated[str, "source"]] = None,
) -> (cadence.Address, AccountKey, Signer):
pub, priv = random_key_pair(SignAlgo.ECDSA_P256)
"""
Generate a random account.
Parameters
----------
client: AccessAPI
The client to use to create the account.
ctx: Config
The configuration to use.
contracts: dict[str, str]
The contracts to use for the account.
account_key = AccountKey(
public_key=pub, sign_algo=SignAlgo.ECDSA_P256, hash_algo=HashAlgo.SHA3_256
Returns
-------
(cadence.Address, AccountKey, Signer)
The address, account key, and signer for the new account.
"""
address, keys, signers = await random_account_with_weights(
client=client,
ctx=ctx,
weights=[AccountKey.weight_threshold],
contracts=contracts,
)
return address, keys[0], signers[0]


async def random_account_with_weights(
*,
client: AccessAPI,
ctx: Config,
weights: list[int],
contracts: dict[Annotated[str, "name"], Annotated[str, "source"]] = None,
) -> (cadence.Address, list[AccountKey], list[Signer]):
"""
Generate a random account with a given set of weights.
Parameters
----------
client: AccessAPI
The client to use to create the account.
ctx: Config
The configuration to use.
weights: list[int]
The weights to use for the account.
contracts: dict[str, str]
The contracts to use for the account.
Returns
-------
(cadence.Address, list[AccountKey], list[Signer])
The address, account keys, and signers for the new account.
"""
keys = [random_key_pair(SignAlgo.ECDSA_P256) for _ in weights]

account_keys = [
AccountKey(
public_key=keys[i][0],
sign_algo=SignAlgo.ECDSA_P256,
hash_algo=HashAlgo.SHA3_256,
weight=weights[i],
)
for i in range(len(keys))
]

block = await client.get_latest_block()
proposer = await client.get_account_at_latest_block(
Expand All @@ -42,7 +101,7 @@ async def random_account(

tx = (
create_account_template(
keys=[account_key],
keys=account_keys,
contracts=contracts,
reference_block_id=block.id,
payer=ctx.service_account_address,
Expand All @@ -69,10 +128,13 @@ async def random_account(

return (
new_addresses[0],
account_key,
InMemorySigner(
sign_algo=SignAlgo.ECDSA_P256,
hash_algo=HashAlgo.SHA3_256,
private_key_hex=priv.hex(),
),
account_keys,
[
InMemorySigner(
sign_algo=SignAlgo.ECDSA_P256,
hash_algo=HashAlgo.SHA3_256,
private_key_hex=priv.hex(),
)
for _, priv in keys
],
)
7 changes: 6 additions & 1 deletion examples/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ async def run_async(ctx: Config, examples: list[str]) -> Annotated[bool, "Succes

def run():
# last index of string "examples"
example_index = sys.argv.index("examples")
try:
example_index = sys.argv.index("examples")
except ValueError:
# used if run is called without any arguments
# for example when running this file directly without the `poetry run examples` command
example_index = 0
examples = sys.argv[example_index + 1 :]

config_location = pathlib.Path(__file__).parent.resolve().joinpath("./flow.json")
Expand Down
64 changes: 64 additions & 0 deletions examples/user_message_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from flow_py_sdk import (
SignAlgo,
HashAlgo,
InMemorySigner,
InMemoryVerifier,
flow_client,
AccountKey,
utils,
)
from examples.common.utils import random_account, random_account_with_weights
from examples.common import Example, Config


# -------------------------------------------------------------------------
# Sign and verify a user message
# this example shows how to verify a message was signed by the owner(s) of an account
# -------------------------------------------------------------------------
class SignAndVerifyUserMessageExample(Example):
def __init__(self) -> None:
super().__init__(
tag="V.1.", name="SignAndVerifyUserMessageExample", sort_order=601
)

async def run(self, ctx: Config):
# generate a random account with 3 keys
async with flow_client(
host=ctx.access_node_host, port=ctx.access_node_port
) as client:
# create account with tree half weight keys
# only two signatures are required to sign a message (or a transaction)
account_address, _, account_signers = await random_account_with_weights(
client=client,
ctx=ctx,
weights=[
int(AccountKey.weight_threshold / 2),
int(AccountKey.weight_threshold / 2),
int(AccountKey.weight_threshold / 2),
],
)

# the message to sign. Could include some extra information, like the reference block id or the address.
message = b"Hello World!"

# get two signatures from the account signers
# signer 1
signature = account_signers[0].sign_user_message(message)
c_signature_1 = utils.CompositeSignature(
account_address.hex(), 0, signature.hex()
)

# signer 3
signature = account_signers[2].sign_user_message(message)
c_signature_2 = utils.CompositeSignature(
account_address.hex(), 2, signature.hex()
)

# verify the signature is valid
signature_is_valid = await utils.verify_user_signature(
message=message,
client=client,
composite_signatures=[c_signature_1, c_signature_2],
)

assert signature_is_valid
11 changes: 9 additions & 2 deletions flow_py_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
from .client import flow_client, AccessAPI, entities
from .script import Script
from .exceptions import PySDKError, NotCadenceValueError
from .signer import SignAlgo, HashAlgo, InMemorySigner, Signer
from .signer import (
SignAlgo,
HashAlgo,
InMemorySigner,
InMemoryVerifier,
Signer,
Verifier,
)
from .account_key import AccountKey
from .templates import create_account_template, ContractTemplates
from .templates import create_account_template, TransactionTemplates
from .tx import Tx, ProposalKey, TxSignature, TransactionStatus

logging.getLogger(__name__).addHandler(logging.NullHandler())
4 changes: 3 additions & 1 deletion flow_py_sdk/signer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from flow_py_sdk.signer.hash_algo import HashAlgo
from flow_py_sdk.signer.sign_algo import SignAlgo
from flow_py_sdk.signer.signer import Signer
from flow_py_sdk.signer.signer import Signer, TransactionDomainTag, UserDomainTag
from flow_py_sdk.signer.verifier import Verifier
from flow_py_sdk.signer.in_memory_verifier import InMemoryVerifier
from flow_py_sdk.signer.in_memory_signer import InMemorySigner
21 changes: 16 additions & 5 deletions flow_py_sdk/signer/in_memory_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import ecdsa

from flow_py_sdk.signer.hash_algo import HashAlgo
from flow_py_sdk.signer.in_memory_verifier import InMemoryVerifier
from flow_py_sdk.signer.sign_algo import SignAlgo
from flow_py_sdk.signer.signer import Signer
from flow_py_sdk.signer.verifier import Verifier


class InMemorySigner(Signer):
"""The InMemorySigner used for signing transaction and messaged given a private key hex."""

class InMemorySigner(Signer, Verifier):
def __init__(
self, *, hash_algo: HashAlgo, sign_algo: SignAlgo, private_key_hex: str
) -> None:
Expand All @@ -18,12 +18,23 @@ def __init__(
self.key = ecdsa.SigningKey.from_string(
bytes.fromhex(private_key_hex), curve=sign_algo.get_signing_curve()
)
self.verifier = InMemoryVerifier(
hash_algo=hash_algo,
sign_algo=sign_algo,
public_key_hex=self.key.get_verifying_key().to_string().hex(),
)

def sign(self, message: bytes, tag: Optional[bytes] = None) -> bytes:
hash_ = self._hash_message(message, tag)
return self.key.sign_digest_deterministic(hash_)

def verify(self, signature: bytes, message: bytes, tag: bytes) -> bool:
return self.verifier.verify(signature, message, tag)

def _hash_message(self, message: bytes, tag: Optional[bytes] = None) -> bytes:
m = self.hash_algo.create_hasher()
if tag:
m.update(tag + message)
else:
m.update(message)
hash_ = m.digest()
return self.key.sign_digest_deterministic(hash_)
return m.digest()
33 changes: 33 additions & 0 deletions flow_py_sdk/signer/in_memory_verifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Optional

import ecdsa

from flow_py_sdk.signer.hash_algo import HashAlgo
from flow_py_sdk.signer.sign_algo import SignAlgo
from flow_py_sdk.signer.verifier import Verifier


class InMemoryVerifier(Verifier):
def __init__(
self, *, hash_algo: HashAlgo, sign_algo: SignAlgo, public_key_hex: str
) -> None:
super().__init__()
self.hash_algo = hash_algo
self.key = ecdsa.VerifyingKey.from_string(
bytes.fromhex(public_key_hex), curve=sign_algo.get_signing_curve()
)

def verify(self, signature: bytes, message: bytes, tag: bytes) -> bool:
hash_ = self._hash_message(message, tag)
try:
return self.key.verify_digest(signature, hash_)
except ecdsa.keys.BadSignatureError:
return False

def _hash_message(self, message: bytes, tag: Optional[bytes] = None) -> bytes:
m = self.hash_algo.create_hasher()
if tag:
m.update(tag + message)
else:
m.update(message)
return m.digest()
Loading

0 comments on commit 8b10f4c

Please sign in to comment.