Skip to content

Commit

Permalink
Introduce new MessageSigning scheme and allow to use both at the same…
Browse files Browse the repository at this point in the history
… time (at least for verifying)
  • Loading branch information
xeroc committed Apr 2, 2019
1 parent 77baed1 commit b92f107
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 3 deletions.
169 changes: 168 additions & 1 deletion graphenecommon/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re

from binascii import hexlify, unhexlify
from datetime import datetime

from graphenebase.ecdsa import sign_message, verify_message

Expand All @@ -19,7 +20,7 @@
log = logging.getLogger(__name__)


class Message(AbstractBlockchainInstanceProvider):
class MessageV1(AbstractBlockchainInstanceProvider):
""" Allow to sign and verify Messages that are sigend with a private key
"""

Expand Down Expand Up @@ -187,3 +188,169 @@ def verify(self, **kwargs):
self.plain_message = message

return True


class MessageV2(AbstractBlockchainInstanceProvider):
""" Allow to sign and verify Messages that are sigend with a private key
"""

def __init__(self, message, *args, **kwargs):
self.define_classes()
assert self.account_class
assert self.publickey_class

self.message = message
self.signed_by_account = None
self.signed_by_name = None
self.meta = None
self.plain_message = None

def sign(self, account=None, **kwargs):
""" Sign a message with an account's memo key
:param str account: (optional) the account that owns the bet
(defaults to ``default_account``)
:raises ValueError: If not account for signing is provided
:returns: the signed message encapsulated in a known format
"""
if not account:
if "default_account" in self.blockchain.config:
account = self.blockchain.config["default_account"]
if not account:
raise ValueError("You need to provide an account")

# Data for message
account = self.account_class(account, blockchain_instance=self.blockchain)

# wif key
wif = self.blockchain.wallet.getPrivateKeyForPublicKey(
account["options"]["memo_key"]
)

payload = [
"from",
account["name"],
"key",
account["options"]["memo_key"],
"time",
str(datetime.utcnow()),
"text",
self.message,
]
enc_message = json.dumps(payload)

# signature
signature = hexlify(sign_message(enc_message, wif)).decode("ascii")

return dict(signed=enc_message, payload=payload, signature=signature)

def verify(self, **kwargs):
""" Verify a message with an account's memo key
:param str account: (optional) the account that owns the bet
(defaults to ``default_account``)
:returns: True if the message is verified successfully
:raises InvalidMessageSignature if the signature is not ok
"""
assert isinstance(self.message, dict)

payload = self.message.get("payload")
assert payload
payload_dict = {k[0]: k[1] for k in zip(payload[::2], payload[1::2])}
signature = self.message.get("signature")

account_name = payload_dict.get("from").strip()
memo_key = payload_dict.get("key").strip()

assert account_name
assert memo_key

try:
self.publickey_class(memo_key, prefix=self.blockchain.prefix)
except Exception:
raise InvalidMemoKeyException("The memo key in the message is invalid")

# Load account from blockchain
try:
account = self.account_class(
account_name, blockchain_instance=self.blockchain
)
except AccountDoesNotExistsException:
raise AccountDoesNotExistsException(
"Could not find account {}. Are you connected to the right chain?".format(
account_name
)
)

# Test if memo key is the same as on the blockchain
if not account["options"]["memo_key"] == memo_key:
raise WrongMemoKey(
"Memo Key of account {} on the Blockchain ".format(account["name"])
+ "differs from memo key in the message: {} != {}".format(
account["options"]["memo_key"], memo_key
)
)

# Ensure payload and signed match
assert json.dumps(self.message.get("payload")) == self.message.get("signed")

# Reformat message
enc_message = self.message.get("signed")

# Verify Signature
pubkey = verify_message(enc_message, unhexlify(signature))

# Verify pubky
pk = self.publickey_class(
hexlify(pubkey).decode("ascii"), prefix=self.blockchain.prefix
)
if format(pk, self.blockchain.prefix) != memo_key:
raise InvalidMessageSignature("The signature doesn't match the memo key")

self.signed_by_account = account
self.signed_by_name = account["name"]
self.plain_message = payload_dict.get("text")

return True


class Message(MessageV1, MessageV2):
supported_formats = (MessageV1, MessageV2)
valid_exceptions = (
AccountDoesNotExistsException,
InvalidMessageSignature,
WrongMemoKey,
InvalidMemoKeyException,
)

def __init__(self, message, *args, **kwargs):
for _format in self.supported_formats:
try:
_format.__init__(self, message, *args, **kwargs)
return
except self.valid_exceptions as e:
raise e
except Exception:
pass

def verify(self, **kwargs):
for _format in self.supported_formats:
try:
_format.verify(self, **kwargs)
return
except self.valid_exceptions as e:
raise e
except Exception:
pass

def sign(self, account=None, **kwargs):
for _format in self.supported_formats:
try:
_format.sign(self, account=None, **kwargs)
return
except self.valid_exceptions as e:
raise e
except Exception:
pass
20 changes: 19 additions & 1 deletion tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@
from graphenecommon.committee import Committee as GCommittee
from graphenecommon.block import Block as GBlock, BlockHeader as GBlockHeader
from graphenecommon.blockchain import Blockchain as GBLockchain
from graphenecommon.message import Message as GMessage
from graphenecommon.message import (
Message as GMessage,
MessageV1 as GMessageV1,
MessageV2 as GMessageV2,
)
from graphenecommon.blockchainobject import ObjectCache, BlockchainObject
from graphenecommon.price import Price as GPrice
from graphenecommon.wallet import Wallet as GWallet
Expand Down Expand Up @@ -258,6 +262,20 @@ def define_classes(self):
self.publickey_class = PublicKey


@BlockchainInstance.inject
class MessageV1(GMessageV1):
def define_classes(self):
self.account_class = Account
self.publickey_class = PublicKey


@BlockchainInstance.inject
class MessageV2(GMessageV2):
def define_classes(self):
self.account_class = Account
self.publickey_class = PublicKey


@BlockchainInstance.inject
class Price(GPrice):
def define_classes(self):
Expand Down
20 changes: 19 additions & 1 deletion tests/test_message.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import unittest
from .fixtures import fixture_data, Message
from .fixtures import fixture_data, Message, MessageV1, MessageV2
from pprint import pprint
from graphenecommon.exceptions import InvalidMessageSignature


Expand Down Expand Up @@ -40,3 +41,20 @@ def test_verify_message(self):
"2034f601e175a25cf9f60a828650301f57c9efab53929b6a82fb413feb8a786fcb3ba4238dd8bece03aee38526ee363324d43944d4a3f9dc624fbe53ef5f0c9a5e\n"
"-----END GRAPHENE SIGNED MESSAGE-----"
).verify()

def test_v2_enc(self):
m = MessageV2("foobar")
c = m.sign(account="init0")
v = MessageV2(c)
v.verify()

def test_v2andv1_enc(self):
m = MessageV2("foobar")
c = m.sign(account="init0")
v = Message(c)
v.verify()

m = MessageV1("foobar")
c = m.sign(account="init0")
v = Message(c)
v.verify()

0 comments on commit b92f107

Please sign in to comment.