okay fine

This commit is contained in:
pacnpal
2024-11-03 17:47:26 +00:00
parent 01c6004a79
commit 27eb239e97
10020 changed files with 1935769 additions and 2364 deletions

View File

@@ -0,0 +1,450 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import traceback
try:
# PyPy 7.3.3 lacks this
from _hashlib import HASH # noqa: F401
except ImportError:
# monkey patch it:
import _hashlib
_hashlib.HASH = _hashlib.Hash
try:
from mnemonic import Mnemonic
from autobahn.xbr._mnemonic import mnemonic_to_private_key
# monkey patch eth_abi for master branch (which we need for python 3.11)
# https://github.com/ethereum/eth-abi/blob/master/docs/release_notes.rst#breaking-changes
# https://github.com/ethereum/eth-abi/pull/161
# ImportError: cannot import name 'encode_single' from 'eth_abi' (/home/oberstet/cpy311_2/lib/python3.11/site-packages/eth_abi/__init__.py)
import eth_abi
if not hasattr(eth_abi, 'encode_abi') and hasattr(eth_abi, 'encode'):
eth_abi.encode_abi = eth_abi.encode
if not hasattr(eth_abi, 'encode_single') and hasattr(eth_abi, 'encode'):
eth_abi.encode_single = lambda typ, val: eth_abi.encode([typ], [val])
# monkey patch, see:
# https://github.com/ethereum/web3.py/issues/1201
# https://github.com/ethereum/eth-abi/pull/88
if not hasattr(eth_abi, 'collapse_type'):
def collapse_type(base, sub, arrlist):
return base + sub + ''.join(map(repr, arrlist))
eth_abi.collapse_type = collapse_type
if not hasattr(eth_abi, 'process_type'):
from eth_abi.grammar import (
TupleType,
normalize,
parse,
)
def process_type(type_str):
normalized_type_str = normalize(type_str)
abi_type = parse(normalized_type_str)
type_str_repr = repr(type_str)
if type_str != normalized_type_str:
type_str_repr = '{} (normalized to {})'.format(
type_str_repr,
repr(normalized_type_str),
)
if isinstance(abi_type, TupleType):
raise ValueError("Cannot process type {}: tuple types not supported".format(type_str_repr, ))
abi_type.validate()
sub = abi_type.sub
if isinstance(sub, tuple):
sub = 'x'.join(map(str, sub))
elif isinstance(sub, int):
sub = str(sub)
else:
sub = ''
arrlist = abi_type.arrlist
if isinstance(arrlist, tuple):
arrlist = list(map(list, arrlist))
else:
arrlist = []
return abi_type.base, sub, arrlist
eth_abi.process_type = process_type
# monkey patch web3 for master branch / upcoming v6 (which we need for python 3.11)
# AttributeError: type object 'Web3' has no attribute 'toChecksumAddress'. Did you mean: 'to_checksum_address'?
import web3
if not hasattr(web3.Web3, 'toChecksumAddress') and hasattr(web3.Web3, 'to_checksum_address'):
web3.Web3.toChecksumAddress = web3.Web3.to_checksum_address
if not hasattr(web3.Web3, 'isChecksumAddress') and hasattr(web3.Web3, 'is_checksum_address'):
web3.Web3.isChecksumAddress = web3.Web3.is_checksum_address
if not hasattr(web3.Web3, 'isConnected') and hasattr(web3.Web3, 'is_connected'):
web3.Web3.isConnected = web3.Web3.is_connected
if not hasattr(web3.Web3, 'privateKeyToAccount') and hasattr(web3.middleware.signing, 'private_key_to_account'):
web3.Web3.privateKeyToAccount = web3.middleware.signing.private_key_to_account
import ens
if not hasattr(ens, 'main') and hasattr(ens, 'ens'):
ens.main = ens.ens
import eth_account
from autobahn.xbr._abi import XBR_TOKEN_ABI, XBR_NETWORK_ABI, XBR_MARKET_ABI, XBR_CATALOG_ABI, XBR_CHANNEL_ABI # noqa
from autobahn.xbr._abi import XBR_DEBUG_TOKEN_ADDR, XBR_DEBUG_NETWORK_ADDR, XBR_DEBUG_MARKET_ADDR, XBR_DEBUG_CATALOG_ADDR, XBR_DEBUG_CHANNEL_ADDR # noqa
from autobahn.xbr._abi import XBR_DEBUG_TOKEN_ADDR_SRC, XBR_DEBUG_NETWORK_ADDR_SRC, XBR_DEBUG_MARKET_ADDR_SRC, XBR_DEBUG_CATALOG_ADDR_SRC, XBR_DEBUG_CHANNEL_ADDR_SRC # noqa
from autobahn.xbr._interfaces import IMarketMaker, IProvider, IConsumer, ISeller, IBuyer, IDelegate # noqa
from autobahn.xbr._util import make_w3, pack_uint256, unpack_uint256 # noqa
from autobahn.xbr._eip712_certificate import EIP712Certificate # noqa
from autobahn.xbr._eip712_certificate_chain import parse_certificate_chain # noqa
from autobahn.xbr._eip712_authority_certificate import sign_eip712_authority_certificate, \
recover_eip712_authority_certificate, create_eip712_authority_certificate, EIP712AuthorityCertificate # noqa
from autobahn.xbr._eip712_delegate_certificate import sign_eip712_delegate_certificate, \
recover_eip712_delegate_certificate, create_eip712_delegate_certificate, EIP712DelegateCertificate # noqa
from autobahn.xbr._eip712_member_register import sign_eip712_member_register, recover_eip712_member_register # noqa
from autobahn.xbr._eip712_member_login import sign_eip712_member_login, recover_eip712_member_login # noqa
from autobahn.xbr._eip712_market_create import sign_eip712_market_create, recover_eip712_market_create # noqa
from autobahn.xbr._eip712_market_join import sign_eip712_market_join, recover_eip712_market_join # noqa
from autobahn.xbr._eip712_catalog_create import sign_eip712_catalog_create, recover_eip712_catalog_create # noqa
from autobahn.xbr._eip712_api_publish import sign_eip712_api_publish, recover_eip712_api_publish # noqa
from autobahn.xbr._eip712_consent import sign_eip712_consent, recover_eip712_consent # noqa
from autobahn.xbr._eip712_channel_open import sign_eip712_channel_open, recover_eip712_channel_open # noqa
from autobahn.xbr._eip712_channel_close import sign_eip712_channel_close, recover_eip712_channel_close # noqa
from autobahn.xbr._eip712_market_member_login import sign_eip712_market_member_login, \
recover_eip712_market_member_login # noqa
from autobahn.xbr._eip712_base import is_address, is_chain_id, is_block_number, is_signature, \
is_cs_pubkey, is_bytes16, is_eth_privkey # noqa
from autobahn.xbr._blockchain import SimpleBlockchain # noqa
from autobahn.xbr._seller import SimpleSeller, KeySeries # noqa
from autobahn.xbr._buyer import SimpleBuyer # noqa
from autobahn.xbr._config import load_or_create_profile, UserConfig, Profile # noqa
from autobahn.xbr._schema import FbsSchema, FbsObject, FbsType, FbsRPCCall, FbsEnum, FbsService, FbsEnumValue, \
FbsAttribute, FbsField, FbsRepository # noqa
from autobahn.xbr._wallet import stretch_argon2_secret, expand_argon2_secret, pkm_from_argon2_secret # noqa
from autobahn.xbr._frealm import FederatedRealm, Seeder # noqa
from autobahn.xbr._secmod import EthereumKey, SecurityModuleMemory # noqa
from autobahn.xbr._userkey import UserKey # noqa
HAS_XBR = True
xbrtoken = None
"""
Contract instance of XBRToken.
"""
xbrnetwork = None
"""
Contract instance of XBRNetwork.
"""
xbrmarket = None
"""
Contract instance of XBRMarket.
"""
xbrcatalog = None
"""
Contract instance of XBRMarket.
"""
xbrchannel = None
"""
Contract instance of XBRMarket.
"""
def setProvider(_w3):
"""
The XBR library must be initialized (once) first by setting the Web3 provider
using this function.
"""
global xbrtoken
global xbrnetwork
global xbrmarket
global xbrcatalog
global xbrchannel
# print('Provider set - xbrtoken={}'.format(XBR_DEBUG_TOKEN_ADDR))
xbrtoken = _w3.eth.contract(address=XBR_DEBUG_TOKEN_ADDR, abi=XBR_TOKEN_ABI)
# print('Provider set - xbrnetwork={}'.format(XBR_DEBUG_NETWORK_ADDR))
xbrnetwork = _w3.eth.contract(address=XBR_DEBUG_NETWORK_ADDR, abi=XBR_NETWORK_ABI)
# print('Provider set - xbrmarket={}'.format(XBR_DEBUG_MARKET_ADDR))
xbrmarket = _w3.eth.contract(address=XBR_DEBUG_MARKET_ADDR, abi=XBR_MARKET_ABI)
# print('Provider set - xbrcatalog={}'.format(XBR_DEBUG_CATALOG_ADDR))
xbrcatalog = _w3.eth.contract(address=XBR_DEBUG_CATALOG_ADDR, abi=XBR_CATALOG_ABI)
# print('Provider set - xbrchannel={}'.format(XBR_DEBUG_CHANNEL_ADDR))
xbrchannel = _w3.eth.contract(address=XBR_DEBUG_CHANNEL_ADDR, abi=XBR_CHANNEL_ABI)
class MemberLevel(object):
"""
XBR Network member levels.
"""
NONE = 0
ACTIVE = 1
VERIFIED = 2
RETIRED = 3
PENALTY = 4
BLOCKED = 5
class NodeType(object):
"""
XBR Cloud node types.
"""
NONE = 0
MASTER = 1
CORE = 2
EDGE = 3
class ChannelType(object):
"""
Type of a XBR off-chain channel: paying channel (for provider delegates selling data services) or payment channel (for consumer delegates buying data services).
"""
NONE = 0
"""
Unset
"""
PAYMENT = 1
"""
Payment channel: from buyer/consumer delegate to maker maker.
"""
PAYING = 2
"""
Paying channel: from market maker to seller/provider delegate.
"""
class ActorType(object):
"""
XBR Market Actor type.
"""
NONE = 0
"""
Unset
"""
PROVIDER = 1
"""
Actor is a XBR Provider.
"""
CONSUMER = 2
"""
Actor is a XBR Consumer.
"""
PROVIDER_CONSUMER = 3
"""
Actor is both a XBR Provider and XBR Consumer.
"""
def generate_seedphrase(strength=128, language='english') -> str:
"""
Generate a new BIP-39 mnemonic seed phrase for use in Ethereum (Metamask, etc).
See:
* https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
* https://github.com/trezor/python-mnemonic
:param strength: Strength of seed phrase in bits, one of the following ``[128, 160, 192, 224, 256]``,
generating seed phrase of 12 - 24 words inlength.
:return: Newly generated seed phrase (in english).
"""
return Mnemonic(language).generate(strength)
def check_seedphrase(seedphrase: str, language: str = 'english'):
"""
Check a BIP-39 mnemonic seed phrase.
:param seedphrase: The BIP-39 seedphrase from which to derive the account.
:param language: The BIP-39 user language to generate the seedphrase for.
:return:
"""
return Mnemonic(language).check(seedphrase)
def account_from_seedphrase(seedphrase: str, index: int = 0) -> eth_account.account.Account:
"""
Create an account from the given BIP-39 mnemonic seed phrase.
:param seedphrase: The BIP-39 seedphrase from which to derive the account.
:param index: The account index in account hierarchy defined by the seedphrase.
:return: The new Eth account object
"""
from web3.middleware.signing import private_key_to_account
derivation_path = "m/44'/60'/0'/0/{}".format(index)
key = mnemonic_to_private_key(seedphrase, str_derivation_path=derivation_path)
account = private_key_to_account(key)
return account
def account_from_ethkey(ethkey: bytes) -> eth_account.account.Account:
"""
Create an account from the private key seed.
:param ethkey: The Ethereum private key seed (32 octets).
:return: The new Eth account object
"""
from web3.middleware.signing import private_key_to_account
assert len(ethkey) == 32
account = private_key_to_account(ethkey)
return account
ASCII_BOMB = r"""
_ ._ _ , _ ._
(_ ' ( ` )_ .__)
( ( ( ) `) ) _)
(__ (_ (_ . _) _) ,__)
`~~`\ ' . /`~~`
; ;
/ \
_____________/_ __ \_____________
"""
__all__ = (
'HAS_XBR',
'XBR_TOKEN_ABI',
'XBR_NETWORK_ABI',
'XBR_MARKET_ABI',
'XBR_CATALOG_ABI',
'XBR_CHANNEL_ABI',
'xbrtoken',
'xbrnetwork',
'xbrmarket',
'xbrcatalog',
'xbrchannel',
'setProvider',
'make_w3',
'pack_uint256',
'unpack_uint256',
'generate_seedphrase',
'check_seedphrase',
'account_from_seedphrase',
'ASCII_BOMB',
'EIP712Certificate',
'EIP712AuthorityCertificate',
'EIP712DelegateCertificate',
'parse_certificate_chain',
'create_eip712_authority_certificate',
'sign_eip712_authority_certificate',
'recover_eip712_authority_certificate',
'create_eip712_delegate_certificate',
'sign_eip712_delegate_certificate',
'recover_eip712_delegate_certificate',
'sign_eip712_member_register',
'recover_eip712_member_register',
'sign_eip712_member_login',
'recover_eip712_member_login',
'sign_eip712_market_create',
'recover_eip712_market_create',
'sign_eip712_market_join',
'recover_eip712_market_join',
'sign_eip712_catalog_create',
'recover_eip712_catalog_create',
'sign_eip712_api_publish',
'recover_eip712_api_publish',
'sign_eip712_consent',
'recover_eip712_consent',
'sign_eip712_channel_open',
'recover_eip712_channel_open',
'sign_eip712_channel_close',
'recover_eip712_channel_close',
'sign_eip712_market_member_login',
'recover_eip712_market_member_login',
'is_bytes16',
'is_cs_pubkey',
'is_signature',
'is_chain_id',
'is_eth_privkey',
'is_block_number',
'is_address',
'load_or_create_profile',
'UserConfig',
'Profile',
'UserKey',
'MemberLevel',
'ActorType',
'ChannelType',
'NodeType',
'KeySeries',
'SimpleBlockchain',
'SimpleSeller',
'SimpleBuyer',
'IMarketMaker',
'IProvider',
'IConsumer',
'ISeller',
'IBuyer',
'IDelegate',
'FbsRepository',
'FbsSchema',
'FbsService',
'FbsType',
'FbsObject',
'FbsEnum',
'FbsEnumValue',
'FbsRPCCall',
'FbsAttribute',
'FbsField',
'stretch_argon2_secret',
'expand_argon2_secret',
'pkm_from_argon2_secret',
'FederatedRealm',
'Seeder',
'EthereumKey',
'SecurityModuleMemory',
)
except (ImportError, FileNotFoundError) as e:
import sys
traceback.print_tb(e.__traceback__, file=sys.stderr)
sys.stderr.write(str(e))
sys.stderr.flush()
HAS_XBR = False
__all__ = ('HAS_XBR',)

View File

@@ -0,0 +1,156 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
import json
import binascii
import pkg_resources
os***REMOVED***iron['ETH_HASH_BACKEND'] = 'pycryptodome'
# from eth_hash.backends.pycryptodome import keccak256 # noqa
# print('Using eth_hash backend {}'.format(keccak256))
import web3
import xbr
XBR_ABI_VERSION = xbr.__version__
"""
XBR Protocol contracts ABI bundle (Python package) version.
"""
#
# Set default XBR contract addresses to
# XBR v20.5.1. @ Rinkeby (https://github.com/crossbario/xbr-protocol/issues/127)
#
if 'XBR_DEBUG_TOKEN_ADDR' in os***REMOVED***iron:
_token_adr = os***REMOVED***iron['XBR_DEBUG_TOKEN_ADDR']
try:
_token_adr = binascii.a2b_hex(_token_adr[2:])
_token_adr = web3.Web3.toChecksumAddress(_token_adr)
except Exception as e:
raise RuntimeError('could not parse Ethereum address for XBR_DEBUG_TOKEN_ADDR={} - {}'.format(_token_adr, e))
XBR_DEBUG_TOKEN_ADDR = _token_adr
XBR_DEBUG_TOKEN_ADDR_SRC = 'envvar $XBR_DEBUG_TOKEN_ADDR'
else:
XBR_DEBUG_TOKEN_ADDR = '[AWS-SECRET-REMOVED]EA'
XBR_DEBUG_TOKEN_ADDR_SRC = 'builtin'
if 'XBR_DEBUG_NETWORK_ADDR' in os***REMOVED***iron:
_netw_adr = os***REMOVED***iron['XBR_DEBUG_NETWORK_ADDR']
try:
_netw_adr = binascii.a2b_hex(_netw_adr[2:])
_netw_adr = web3.Web3.toChecksumAddress(_netw_adr)
except Exception as e:
raise RuntimeError('could not parse Ethereum address for XBR_DEBUG_NETWORK_ADDR={} - {}'.format(_netw_adr, e))
XBR_DEBUG_NETWORK_ADDR = _netw_adr
XBR_DEBUG_NETWORK_ADDR_SRC = 'envvar $XBR_DEBUG_NETWORK_ADDR'
else:
XBR_DEBUG_NETWORK_ADDR = '[AWS-SECRET-REMOVED]Bc'
XBR_DEBUG_NETWORK_ADDR_SRC = 'builtin'
if 'XBR_DEBUG_DOMAIN_ADDR' in os***REMOVED***iron:
_domain_adr = os***REMOVED***iron['XBR_DEBUG_DOMAIN_ADDR']
try:
_domain_adr = binascii.a2b_hex(_domain_adr[2:])
_domain_adr = web3.Web3.toChecksumAddress(_domain_adr)
except Exception as e:
raise RuntimeError('could not parse Ethereum address for XBR_DEBUG_DOMAIN_ADDR={} - {}'.format(_domain_adr, e))
XBR_DEBUG_DOMAIN_ADDR = _domain_adr
XBR_DEBUG_DOMAIN_ADDR_SRC = 'envvar $XBR_DEBUG_DOMAIN_ADDR'
else:
XBR_DEBUG_DOMAIN_ADDR = '[AWS-SECRET-REMOVED]bC'
XBR_DEBUG_DOMAIN_ADDR_SRC = 'builtin'
if 'XBR_DEBUG_CATALOG_ADDR' in os***REMOVED***iron:
_ctlg_adr = os***REMOVED***iron['XBR_DEBUG_CATALOG_ADDR']
try:
_ctlg_adr = binascii.a2b_hex(_ctlg_adr[2:])
_ctlg_adr = web3.Web3.toChecksumAddress(_ctlg_adr)
except Exception as e:
raise RuntimeError('could not parse Ethereum address for XBR_DEBUG_CATALOG_ADDR={} - {}'.format(_ctlg_adr, e))
XBR_DEBUG_CATALOG_ADDR = _ctlg_adr
XBR_DEBUG_CATALOG_ADDR_SRC = 'envvar $XBR_DEBUG_CATALOG_ADDR'
else:
XBR_DEBUG_CATALOG_ADDR = '[AWS-SECRET-REMOVED]66'
XBR_DEBUG_CATALOG_ADDR_SRC = 'builtin'
if 'XBR_DEBUG_MARKET_ADDR' in os***REMOVED***iron:
_mrkt_adr = os***REMOVED***iron['XBR_DEBUG_MARKET_ADDR']
try:
_mrkt_adr = binascii.a2b_hex(_mrkt_adr[2:])
_mrkt_adr = web3.Web3.toChecksumAddress(_mrkt_adr)
except Exception as e:
raise RuntimeError('could not parse Ethereum address for XBR_DEBUG_MARKET_ADDR={} - {}'.format(_mrkt_adr, e))
XBR_DEBUG_MARKET_ADDR = _mrkt_adr
XBR_DEBUG_MARKET_ADDR_SRC = 'envvar $XBR_DEBUG_MARKET_ADDR'
else:
XBR_DEBUG_MARKET_ADDR = '[AWS-SECRET-REMOVED]3d'
XBR_DEBUG_MARKET_ADDR_SRC = 'builtin'
if 'XBR_DEBUG_CHANNEL_ADDR' in os***REMOVED***iron:
_chnl_adr = os***REMOVED***iron['XBR_DEBUG_CHANNEL_ADDR']
try:
_chnl_adr = binascii.a2b_hex(_chnl_adr[2:])
_chnl_adr = web3.Web3.toChecksumAddress(_chnl_adr)
except Exception as e:
raise RuntimeError('could not parse Ethereum address for XBR_DEBUG_CHANNEL_ADDR={} - {}'.format(_chnl_adr, e))
XBR_DEBUG_CHANNEL_ADDR = _chnl_adr
XBR_DEBUG_CHANNEL_ADDR_SRC = 'envvar $XBR_DEBUG_CHANNEL_ADDR'
else:
XBR_DEBUG_CHANNEL_ADDR = '[AWS-SECRET-REMOVED]52'
XBR_DEBUG_CHANNEL_ADDR_SRC = 'builtin'
def _load_json(contract_name):
fn = pkg_resources.resource_filename('xbr', 'abi/{}.json'.format(contract_name))
with open(fn) as f:
data = json.loads(f.read())
return data
#
# XBR contract ABI file names
#
XBR_TOKEN_FN = pkg_resources.resource_filename('xbr', 'abi/XBRToken.json')
XBR_NETWORK_FN = pkg_resources.resource_filename('xbr', 'abi/XBRNetwork.json')
XBR_DOMAIN_FN = pkg_resources.resource_filename('xbr', 'abi/XBRDomain.json')
XBR_CATALOG_FN = pkg_resources.resource_filename('xbr', 'abi/XBRCatalog.json')
XBR_MARKET_FN = pkg_resources.resource_filename('xbr', 'abi/XBRMarket.json')
XBR_CHANNEL_FN = pkg_resources.resource_filename('xbr', 'abi/XBRChannel.json')
#
# XBR contract ABIs
#
XBR_TOKEN_ABI = _load_json('XBRToken')['abi']
XBR_NETWORK_ABI = _load_json('XBRNetwork')['abi']
XBR_DOMAIN_ABI = _load_json('XBRDomain')['abi']
XBR_CATALOG_ABI = _load_json('XBRCatalog')['abi']
XBR_MARKET_ABI = _load_json('XBRMarket')['abi']
XBR_CHANNEL_ABI = _load_json('XBRChannel')['abi']

View File

@@ -0,0 +1,238 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import web3
import txaio
from autobahn import xbr
class SimpleBlockchain(object):
"""
Simple Ethereum blockchain XBR client.
"""
DomainStatus_NULL = 0
DomainStatus_ACTIVE = 1
DomainStatus_CLOSED = 2
NodeType_NULL = 0
NodeType_MASTER = 1
NodeType_CORE = 2
NodeType_EDGE = 3
NodeLicense_NULL = 0
NodeLicense_INFINITE = 1
NodeLicense_FREE = 2
log = None
backgroundCaller = None
def __init__(self, gateway=None):
"""
:param gateway: Optional explicit Ethereum gateway URL to use.
If no explicit gateway is specified, let web3 auto-choose.
:type gateway: str
"""
self.log = txaio.make_logger()
self._gateway = gateway
self._w3 = None
assert self.backgroundCaller is not None
def start(self):
"""
Start the blockchain client using the configured blockchain gateway.
"""
assert self._w3 is None
if self._gateway:
w3 = web3.Web3(web3.Web3.HTTPProvider(self._gateway))
else:
# using automatic provider detection:
from web3.auto import w3
# check we are connected, and check network ID
if not w3.isConnected():
emsg = 'could not connect to Web3/Ethereum at: {}'.format(self._gateway or 'auto')
self.log.warn(emsg)
raise RuntimeError(emsg)
else:
print('connected to network {} at provider "{}"'.format(w3.version.network,
self._gateway or 'auto'))
self._w3 = w3
# set new provider on XBR library
xbr.setProvider(self._w3)
def stop(self):
"""
Stop the blockchain client.
"""
assert self._w3 is not None
self._w3 = None
async def get_market_status(self, market_id):
"""
:param market_id:
:return:
"""
def _get_market_status(_market_id):
owner = xbr.xbrnetwork.functions.getMarketOwner(_market_id).call()
if not owner or owner == '[AWS-SECRET-REMOVED]00':
return None
else:
return {
'owner': owner,
}
return self.backgroundCaller(_get_market_status, market_id)
async def get_domain_status(self, domain_id):
"""
:param domain_id:
:type domain_id: bytes
:return:
:rtype: dict
"""
def _get_domain_status(_domain_id):
status = xbr.xbrnetwork.functions.getDomainStatus(_domain_id).call()
if status == SimpleBlockchain.DomainStatus_NULL:
return None
elif status == SimpleBlockchain.DomainStatus_ACTIVE:
return {'status': 'ACTIVE'}
elif status == SimpleBlockchain.DomainStatus_CLOSED:
return {'status': 'CLOSED'}
return self.backgroundCaller(_get_domain_status, domain_id)
def get_node_status(self, delegate_adr):
"""
:param delegate_adr:
:type delegate_adr: bytes
:return:
:rtype: dict
"""
raise NotImplementedError()
def get_actor_status(self, delegate_adr):
"""
:param delegate_adr:
:type delegate_adr: bytes
:return:
:rtype: dict
"""
raise NotImplementedError()
def get_delegate_status(self, delegate_adr):
"""
:param delegate_adr:
:type delegate_adr: bytes
:return:
:rtype: dict
"""
raise NotImplementedError()
def get_channel_status(self, channel_adr):
"""
:param channel_adr:
:type channel_adr: bytes
:return:
:rtype: dict
"""
raise NotImplementedError()
async def get_member_status(self, member_adr):
"""
:param member_adr:
:type member_adr: bytes
:return:
:rtype: dict
"""
assert type(member_adr) == bytes and len(member_adr) == 20
def _get_member_status(_member_adr):
level = xbr.xbrnetwork.functions.getMemberLevel(member_adr).call()
if not level:
return None
else:
eula = xbr.xbrnetwork.functions.getMemberEula(member_adr).call()
if not eula or eula.strip() == '':
return None
profile = xbr.xbrnetwork.functions.getMemberProfile(member_adr).call()
if not profile or profile.strip() == '':
profile = None
return {
'eula': eula,
'profile': profile,
}
return self.backgroundCaller(_get_member_status, member_adr)
async def get_balances(self, account_adr):
"""
Return current ETH and XBR balances of account with given address.
:param account_adr: Ethereum address of account to get balances for.
:type account_adr: bytes
:return: A dictionary with ``"ETH"`` and ``"XBR"`` keys and respective
current on-chain balances as values.
:rtype: dict
"""
assert type(account_adr) == bytes and len(account_adr) == 20
def _get_balances(_adr):
balance_eth = self._w3.eth.getBalance(_adr)
balance_xbr = xbr.xbrtoken.functions.balanceOf(_adr).call()
return {
'ETH': balance_eth,
'XBR': balance_xbr,
}
return self.backgroundCaller(_get_balances, account_adr)
def get_contract_adrs(self):
"""
Get XBR smart contract addresses.
:return: A dictionary with ``"XBRToken"`` and ``"XBRNetwork"`` keys and Ethereum
contract addresses as values.
:rtype: dict
"""
return {
'XBRToken': xbr.xbrtoken.address,
'XBRNetwork': xbr.xbrnetwork.address,
}

View File

@@ -0,0 +1,636 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import uuid
import binascii
from pprint import pformat
import os
import cbor2
import nacl.secret
import nacl.utils
import nacl.exceptions
import nacl.public
import txaio
from autobahn.wamp.exception import ApplicationError
from autobahn.wamp.protocol import ApplicationSession
from ._util import unpack_uint256, pack_uint256
import eth_keys
from ..util import hl, hlval
from ._eip712_channel_close import sign_eip712_channel_close, recover_eip712_channel_close
class Transaction(object):
def __init__(self, channel, delegate, pubkey, key_id, channel_seq, amount, balance, signature):
self.channel = channel
self.delegate = delegate
self.pubkey = pubkey
self.key_id = key_id
self.channel_seq = channel_seq
self.amount = amount
self.balance = balance
self.signature = signature
def marshal(self):
res = {
'channel': self.channel,
'delegate': self.delegate,
'pubkey': self.pubkey,
'key_id': self.key_id,
'channel_seq': self.channel_seq,
'amount': self.amount,
'balance': self.balance,
'signature': self.signature,
}
return res
def __str__(self):
return pformat(self.marshal())
class SimpleBuyer(object):
"""
Simple XBR buyer component. This component can be used by a XBR buyer delegate to
handle the automated buying of data encryption keys from the XBR market maker.
"""
log = None
def __init__(self, market_maker_adr, buyer_key, max_price):
"""
:param market_maker_adr:
:type market_maker_adr:
:param buyer_key: Consumer delegate (buyer) private Ethereum key.
:type buyer_key: bytes
:param max_price: Maximum price we are willing to buy per key.
:type max_price: int
"""
assert type(market_maker_adr) == bytes and len(market_maker_adr) == 20, 'market_maker_adr must be bytes[20], but got "{}"'.format(market_maker_adr)
assert type(buyer_key) == bytes and len(buyer_key) == 32, 'buyer delegate must be bytes[32], but got "{}"'.format(buyer_key)
assert type(max_price) == int and max_price > 0
self.log = txaio.make_logger()
# market maker address
self._market_maker_adr = market_maker_adr
self._xbrmm_config = None
# buyer delegate raw ethereum private key (32 bytes)
self._pkey_raw = buyer_key
# buyer delegate ethereum private key object
self._pkey = eth_keys.keys.PrivateKey(buyer_key)
# buyer delegate ethereum private account from raw private key
# FIXME
# self._acct = Account.privateKeyToAccount(self._pkey)
self._acct = None
# buyer delegate ethereum account canonical address
self._addr = self._pkey.public_key.to_canonical_address()
# buyer delegate ethereum account canonical checksummed address
# FIXME
# self._caddr = web3.Web3.toChecksumAddress(self._addr)
self._caddr = None
# ephemeral data consumer key
self._receive_key = nacl.public.PrivateKey.generate()
# maximum price per key we are willing to pay
self._max_price = max_price
# will be filled with on-chain payment channel contract, once started
self._channel = None
# channel current (off-chain) balance
self._balance = 0
# channel sequence number
self._seq = 0
# this holds the keys we bought (map: key_id => nacl.secret.SecretBox)
self._keys = {}
self._session = None
self._running = False
# automatically initiate a close of the payment channel when running into
# a transaction failing because of insufficient balance remaining in the channel
self._auto_close_channel = True
# FIXME: poor mans local transaction store
self._transaction_idx = {}
self._transactions = []
async def start(self, session, consumer_id):
"""
Start buying keys to decrypt XBR data by calling ``unwrap()``.
:param session: WAMP session over which to communicate with the XBR market maker.
:type session: :class:`autobahn.wamp.protocol.ApplicationSession`
:param consumer_id: XBR consumer ID.
:type consumer_id: str
:return: Current remaining balance in payment channel.
:rtype: int
"""
assert isinstance(session, ApplicationSession)
assert type(consumer_id) == str
assert not self._running
self._session = session
self._running = True
self.log.debug('Start buying from consumer delegate address {address} (public key 0x{public_key}..)',
address=hl(self._caddr),
public_key=binascii.b2a_hex(self._pkey.public_key[:10]).decode())
try:
self._xbrmm_config = await session.call('xbr.marketmaker.get_config')
# get the currently active (if any) payment channel for the delegate
assert type(self._addr) == bytes and len(self._addr) == 20
self._channel = await session.call('xbr.marketmaker.get_active_payment_channel', self._addr)
if not self._channel:
raise Exception('no active payment channel found')
channel_oid = self._channel['channel_oid']
assert type(channel_oid) == bytes and len(channel_oid) == 16
self._channel_oid = uuid.UUID(bytes=channel_oid)
# get the current (off-chain) balance of the payment channel
payment_balance = await session.call('xbr.marketmaker.get_payment_channel_balance', self._channel_oid.bytes)
except:
session.leave()
raise
# FIXME
if type(payment_balance['remaining']) == bytes:
payment_balance['remaining'] = unpack_uint256(payment_balance['remaining'])
if not payment_balance['remaining'] > 0:
raise Exception('no off-chain balance remaining on payment channel')
self._balance = payment_balance['remaining']
self._seq = payment_balance['seq']
self.log.info('Ok, buyer delegate started [active payment channel {channel_oid} with remaining balance {remaining} at sequence {seq}]',
channel_oid=hl(self._channel_oid), remaining=hlval(self._balance), seq=hlval(self._seq))
return self._balance
async def stop(self):
"""
Stop buying keys.
"""
assert self._running
self._running = False
self.log.info('Ok, buyer delegate stopped.')
async def balance(self):
"""
Return current balance of payment channel:
* ``amount``: The initial amount with which the payment channel was opened.
* ``remaining``: The remaining amount of XBR in the payment channel that can be spent.
* ``inflight``: The amount of XBR allocated to buy transactions that are currently processed.
:return: Current payment balance.
:rtype: dict
"""
assert self._session and self._session.is_attached()
payment_balance = await self._session.call('xbr.marketmaker.get_payment_channel_balance', self._channel['channel_oid'])
return payment_balance
async def open_channel(self, buyer_addr, amount, details=None):
"""
:param amount:
:type amount:
:param details:
:type details:
:return:
:rtype:
"""
assert self._session and self._session.is_attached()
# FIXME
signature = os.urandom(64)
payment_channel = await self._session.call('xbr.marketmaker.open_payment_channel',
buyer_addr,
self._addr,
amount,
signature)
balance = {
'amount': payment_channel['amount'],
'remaining': payment_channel['remaining'],
'inflight': payment_channel['inflight'],
}
return balance
async def close_channel(self, details=None):
"""
Requests to close the currently active payment channel.
:return:
"""
async def unwrap(self, key_id, serializer, ciphertext):
"""
Decrypt XBR data. This functions will potentially make the buyer call the
XBR market maker to buy data encryption keys from the XBR provider.
:param key_id: ID of the data encryption used for decryption
of application payload.
:type key_id: bytes
:param serializer: Application payload serializer.
:type serializer: str
:param ciphertext: Ciphertext of encrypted application payload to
decrypt.
:type ciphertext: bytes
:return: Decrypted application payload.
:rtype: object
"""
assert type(key_id) == bytes and len(key_id) == 16
# FIXME: support more app payload serializers
assert type(serializer) == str and serializer in ['cbor']
assert type(ciphertext) == bytes
market_oid = self._channel['market_oid']
channel_oid = self._channel['channel_oid']
# FIXME
current_block_number = 1
verifying_chain_id = self._xbrmm_config['verifying_chain_id']
verifying_contract_adr = binascii.a2b_hex(self._xbrmm_config['verifying_contract_adr'][2:])
# if we don't have the key, buy it!
if key_id in self._keys:
self.log.debug('Key {key_id} already in key store (or currently being bought).',
key_id=hl(uuid.UUID(bytes=key_id)))
else:
self.log.debug('Key {key_id} not yet in key store - buying key ..', key_id=hl(uuid.UUID(bytes=key_id)))
# mark the key as currently being bought already (the location of code here is multi-entrant)
self._keys[key_id] = False
# get (current) price for key we want to buy
quote = await self._session.call('xbr.marketmaker.get_quote', key_id)
# set price we pay set to the (current) quoted price
amount = unpack_uint256(quote['price'])
self.log.debug('Key {key_id} has current price quote {amount}',
key_id=hl(uuid.UUID(bytes=key_id)), amount=hl(int(amount / 10**18)))
if amount > self._max_price:
raise ApplicationError('xbr.error.max_price_exceeded',
'{}.unwrap() - key {} needed cannot be bought: price {} exceeds maximum price of {}'.format(self.__class__.__name__, uuid.UUID(bytes=key_id), int(amount / 10 ** 18), int(self._max_price / 10 ** 18)))
# check (locally) we have enough balance left in the payment channel to buy the key
balance = self._balance - amount
if balance < 0:
if self._auto_close_channel:
# FIXME: sign last transaction (from persisted local history)
last_tx = None
txns = self.past_transactions()
if txns:
last_tx = txns[0]
if last_tx:
# tx1 is the delegate portion, and tx2 is the market maker portion:
# tx1, tx2 = last_tx
# close_adr = tx1.channel
# close_seq = tx1.channel_seq
# close_balance = tx1.balance
# close_is_final = True
close_seq = self._seq
close_balance = self._balance
close_is_final = True
signature = sign_eip712_channel_close(self._pkey_raw,
verifying_chain_id,
verifying_contract_adr,
current_block_number,
market_oid,
channel_oid,
close_seq,
close_balance,
close_is_final)
self.log.debug('auto-closing payment channel {channel_oid} [close_seq={close_seq}, close_balance={close_balance}, close_is_final={close_is_final}]',
channel_oid=uuid.UUID(bytes=channel_oid),
close_seq=close_seq,
close_balance=int(close_balance / 10**18),
close_is_final=close_is_final)
# call market maker to initiate closing of payment channel
await self._session.call('xbr.marketmaker.close_channel',
channel_oid,
verifying_chain_id,
current_block_number,
verifying_contract_adr,
pack_uint256(close_balance),
close_seq,
close_is_final,
signature)
# FIXME: wait for and acquire new payment channel instead of bailing out ..
raise ApplicationError('xbr.error.channel_closed',
'{}.unwrap() - key {} cannot be bought: payment channel {} ran empty and we initiated close at remaining balance of {}'.format(self.__class__.__name__,
uuid.UUID(bytes=key_id),
channel_oid,
int(close_balance / 10 ** 18)))
raise ApplicationError('xbr.error.insufficient_balance',
'{}.unwrap() - key {} cannot be bought: insufficient balance {} in payment channel for amount {}'.format(self.__class__.__name__,
uuid.UUID(bytes=key_id),
int(self._balance / 10 ** 18),
int(amount / 10 ** 18)))
buyer_pubkey = self._receive_key.public_key.encode(encoder=nacl.encoding.RawEncoder)
channel_seq = self._seq + 1
is_final = False
# XBRSIG[1/8]: compute EIP712 typed data signature
signature = sign_eip712_channel_close(self._pkey_raw, verifying_chain_id, verifying_contract_adr,
current_block_number, market_oid, channel_oid, channel_seq,
balance, is_final)
# persist 1st phase of the transaction locally
self._save_transaction_phase1(channel_oid, self._addr, buyer_pubkey, key_id, channel_seq, amount, balance, signature)
# call the market maker to buy the key
try:
receipt = await self._session.call('xbr.marketmaker.buy_key',
self._addr,
buyer_pubkey,
key_id,
channel_oid,
channel_seq,
pack_uint256(amount),
pack_uint256(balance),
signature)
except ApplicationError as e:
if e.error == 'xbr.error.channel_closed':
self.stop()
raise e
except Exception as e:
self.log.error('Encountered error while calling market maker to buy key!')
self.log.failure()
self._keys[key_id] = e
raise e
# XBRSIG[8/8]: check market maker signature
marketmaker_signature = receipt['signature']
marketmaker_channel_seq = receipt['channel_seq']
marketmaker_amount_paid = unpack_uint256(receipt['amount_paid'])
marketmaker_remaining = unpack_uint256(receipt['remaining'])
marketmaker_inflight = unpack_uint256(receipt['inflight'])
signer_address = recover_eip712_channel_close(verifying_chain_id, verifying_contract_adr,
current_block_number, market_oid, channel_oid,
marketmaker_channel_seq, marketmaker_remaining,
False, marketmaker_signature)
if signer_address != self._market_maker_adr:
self.log.warn('{klass}.unwrap()::XBRSIG[8/8] - EIP712 signature invalid: signer_address={signer_address}, delegate_adr={delegate_adr}',
klass=self.__class__.__name__,
signer_address=hl(binascii.b2a_hex(signer_address).decode()),
delegate_adr=hl(binascii.b2a_hex(self._market_maker_adr).decode()))
raise ApplicationError('xbr.error.invalid_signature',
'{}.unwrap()::XBRSIG[8/8] - EIP712 signature invalid or not signed by market maker'.format(self.__class__.__name__))
if self._seq + 1 != marketmaker_channel_seq:
raise ApplicationError('xbr.error.invalid_transaction',
'{}.buy_key(): invalid transaction (channel sequence number mismatch - expected {}, but got {})'.format(self.__class__.__name__, self._seq, receipt['channel_seq']))
if self._balance - amount != marketmaker_remaining:
raise ApplicationError('xbr.error.invalid_transaction',
'{}.buy_key(): invalid transaction (channel remaining amount mismatch - expected {}, but got {})'.format(self.__class__.__name__, self._balance - amount, receipt['remaining']))
self._seq = marketmaker_channel_seq
self._balance = marketmaker_remaining
# persist 2nd phase of the transaction locally
self._save_transaction_phase2(channel_oid, self._market_maker_adr, buyer_pubkey, key_id, marketmaker_channel_seq,
marketmaker_amount_paid, marketmaker_remaining, marketmaker_signature)
# unseal the data encryption key
sealed_key = receipt['sealed_key']
unseal_box = nacl.public.SealedBox(self._receive_key)
try:
key = unseal_box.decrypt(sealed_key)
except nacl.exceptions.CryptoError as e:
self._keys[key_id] = e
raise ApplicationError('xbr.error.decryption_failed', '{}.unwrap() - could not unseal data encryption key: {}'.format(self.__class__.__name__, e))
# remember the key, so we can use it to actually decrypt application payload data
self._keys[key_id] = nacl.secret.SecretBox(key)
transactions_count = self.count_transactions()
self.log.info(
'{klass}.unwrap() - {tx_type} key {key_id} bought for {amount_paid} [payment_channel={payment_channel}, remaining={remaining}, inflight={inflight}, buyer_pubkey={buyer_pubkey}, transactions={transactions}]',
klass=self.__class__.__name__,
tx_type=hl('XBR BUY ', color='magenta'),
key_id=hl(uuid.UUID(bytes=key_id)),
amount_paid=hl(str(int(marketmaker_amount_paid / 10 ** 18)) + ' XBR', color='magenta'),
payment_channel=hl(binascii.b2a_hex(receipt['payment_channel']).decode()),
remaining=hl(int(marketmaker_remaining / 10 ** 18)),
inflight=hl(int(marketmaker_inflight / 10 ** 18)),
buyer_pubkey=hl(binascii.b2a_hex(buyer_pubkey).decode()),
transactions=transactions_count)
# if the key is already being bought, wait until the one buying path of execution has succeeded and done
log_counter = 0
while self._keys[key_id] is False:
if log_counter % 100:
self.log.debug('{klass}.unwrap() - waiting for key "{key_id}" currently being bought ..',
klass=self.__class__.__name__, key_id=hl(uuid.UUID(bytes=key_id)))
log_counter += 1
await txaio.sleep(.2)
# check if the key buying failed and fail the unwrapping in turn
if isinstance(self._keys[key_id], Exception):
e = self._keys[key_id]
raise e
# now that we have the data encryption key, decrypt the application payload
# the decryption key here is an instance of nacl.secret.SecretBox
try:
message = self._keys[key_id].decrypt(ciphertext)
except nacl.exceptions.CryptoError as e:
# Decryption failed. Ciphertext failed verification
raise ApplicationError('xbr.error.decryption_failed', '{}.unwrap() - failed to unwrap encrypted data: {}'.format(self.__class__.__name__, e))
# deserialize the application payload
# FIXME: support more app payload serializers
try:
payload = cbor2.loads(message)
except cbor2.decoder.CBORDecodeError as e:
# premature end of stream (expected to read 4187 bytes, got 27 instead)
raise ApplicationError('xbr.error.deserialization_failed', '{}.unwrap() - failed to deserialize application payload: {}'.format(self.__class__.__name__, e))
return payload
def _save_transaction_phase1(self, channel_oid, delegate_adr, buyer_pubkey, key_id, channel_seq, amount, balance, signature):
"""
:param channel_oid:
:param delegate_adr:
:param buyer_pubkey:
:param key_id:
:param channel_seq:
:param amount:
:param balance:
:param signature:
:return:
"""
if key_id in self._transaction_idx:
raise RuntimeError('save_transaction_phase1: duplicate transaction for key 0x{}'.format(binascii.b2a_hex(key_id)))
tx1 = Transaction(channel_oid, delegate_adr, buyer_pubkey, key_id, channel_seq, amount, balance, signature)
key_idx = len(self._transactions)
self._transactions.append([tx1, None])
self._transaction_idx[key_id] = key_idx
def _save_transaction_phase2(self, channel_oid, delegate_adr, buyer_pubkey, key_id, channel_seq, amount, balance, signature):
"""
:param channel_oid:
:param delegate_adr:
:param buyer_pubkey:
:param key_id:
:param channel_seq:
:param amount:
:param balance:
:param signature:
:return:
"""
if key_id not in self._transaction_idx:
raise RuntimeError('save_transaction_phase2: transaction for key 0x{} not found'.format(binascii.b2a_hex(key_id)))
key_idx = self._transaction_idx[key_id]
if self._transactions[key_idx][1]:
raise RuntimeError(
'save_transaction_phase2: duplicate transaction for key 0x{}'.format(binascii.b2a_hex(key_id)))
tx1 = self._transactions[key_idx][0]
tx2 = Transaction(channel_oid, delegate_adr, buyer_pubkey, key_id, channel_seq, amount, balance, signature)
assert tx1.channel == tx2.channel
# assert tx1.delegate == tx2.delegate
assert tx1.pubkey == tx2.pubkey
assert tx1.key_id == tx2.key_id
assert tx1.channel_seq == tx2.channel_seq
assert tx1.amount == tx2.amount
assert tx1.balance == tx2.balance
# note: signatures will differ (obviously)!
assert tx1.signature != tx2.signature
self._transactions[key_idx][1] = tx2
def past_transactions(self, filter_complete=True, limit=1):
"""
:param filter_complete:
:param limit:
:return:
"""
assert type(filter_complete) == bool
assert type(limit) == int and limit > 0
n = 0
res = []
while n < limit:
if len(self._transactions) > n:
tx = self._transactions[-n]
if not filter_complete or (tx[0] and tx[1]):
res.append(tx)
n += 1
else:
break
return res
def count_transactions(self):
"""
:return:
"""
res = {
'complete': 0,
'pending': 0,
}
for tx1, tx2 in self._transactions:
if tx1 and tx2:
res['complete'] += 1
else:
res['pending'] += 1
return res
def get_transaction(self, key_id):
"""
:param key_id:
:return:
"""
idx = self._transaction_idx.get(key_id, None)
if idx:
return self._transactions[idx]
def is_complete(self, key_id):
"""
:param key_id:
:return:
"""
idx = self._transaction_idx.get(key_id, None)
if idx:
tx1, tx2 = self._transactions[idx]
return tx1 and tx2
return False

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,588 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
import io
import sys
import uuid
import struct
import binascii
import configparser
from typing import Optional, List, Dict
import click
import nacl
import web3
import numpy as np
from time import time_ns
from eth_utils.conversions import hexstr_if_str, to_hex
from autobahn.websocket.util import parse_url
from autobahn.xbr._wallet import pkm_from_argon2_secret
_HAS_COLOR_TERM = False
try:
import colorama
# https://github.com/tartley/colorama/issues/48
term = None
if sys.platform == 'win32' and 'TERM' in os***REMOVED***iron:
term = os***REMOVED***iron.pop('TERM')
colorama.init()
_HAS_COLOR_TERM = True
if term:
os***REMOVED***iron['TERM'] = term
except ImportError:
pass
class Profile(object):
"""
User profile, stored as named section in ``${HOME}/.xbrnetwork/config.ini``:
.. code-block:: INI
[default]
# username used with this profile
username=joedoe
# user email used with the profile (e.g. for verification emails)
email=joe.doe@example.com
# XBR network node used as a directory server and gateway to XBR smart contracts
network_url=ws://localhost:8090/ws
# WAMP realm on network node, usually "xbrnetwork"
network_realm=xbrnetwork
# user private WAMP-cryptosign key (for client authentication)
[AWS-SECRET-REMOVED]179d46aab3d59ec5d93a15286b949eb6
# user private Ethereum key (for signing transactions and e2e data encryption)
[AWS-SECRET-REMOVED]37637b1076dd8cf6fefde13983abaa2ef
"""
def __init__(self,
path: Optional[str] = None,
name: Optional[str] = None,
member_adr: Optional[str] = None,
ethkey: Optional[bytes] = None,
cskey: Optional[bytes] = None,
username: Optional[str] = None,
email: Optional[str] = None,
network_url: Optional[str] = None,
network_realm: Optional[str] = None,
member_oid: Optional[uuid.UUID] = None,
vaction_oid: Optional[uuid.UUID] = None,
vaction_requested: Optional[np.datetime64] = None,
vaction_verified: Optional[np.datetime64] = None,
data_url: Optional[str] = None,
data_realm: Optional[str] = None,
infura_url: Optional[str] = None,
infura_network: Optional[str] = None,
infura_key: Optional[str] = None,
infura_secret: Optional[str] = None):
"""
:param path:
:param name:
:param member_adr:
:param ethkey:
:param cskey:
:param username:
:param email:
:param network_url:
:param network_realm:
:param member_oid:
:param vaction_oid:
:param vaction_requested:
:param vaction_verified:
:param data_url:
:param data_realm:
:param infura_url:
:param infura_network:
:param infura_key:
:param infura_secret:
"""
from txaio import make_logger
self.log = make_logger()
self.path = path
self.name = name
self.member_adr = member_adr
self.ethkey = ethkey
self.cskey = cskey
self.username = username
self.email = email
self.network_url = network_url
self.network_realm = network_realm
self.member_oid = member_oid
self.vaction_oid = vaction_oid
self.vaction_requested = vaction_requested
self.vaction_verified = vaction_verified
self.data_url = data_url
self.data_realm = data_realm
self.infura_url = infura_url
self.infura_network = infura_network
self.infura_key = infura_key
self.infura_secret = infura_secret
def marshal(self):
obj = {}
obj['member_adr'] = self.member_adr or ''
obj['ethkey'] = '0x{}'.format(binascii.b2a_hex(self.ethkey).decode()) if self.ethkey else ''
obj['cskey'] = '0x{}'.format(binascii.b2a_hex(self.cskey).decode()) if self.cskey else ''
obj['username'] = self.username or ''
obj['email'] = self.email or ''
obj['network_url'] = self.network_url or ''
obj['network_realm'] = self.network_realm or ''
obj['member_oid'] = str(self.member_oid) if self.member_oid else ''
obj['vaction_oid'] = str(self.vaction_oid) if self.vaction_oid else ''
obj['vaction_requested'] = str(self.vaction_requested) if self.vaction_requested else ''
obj['vaction_verified'] = str(self.vaction_verified) if self.vaction_verified else ''
obj['data_url'] = self.data_url or ''
obj['data_realm'] = self.data_realm or ''
obj['infura_url'] = self.infura_url or ''
obj['infura_network'] = self.infura_network or ''
obj['infura_key'] = self.infura_key or ''
obj['infura_secret'] = self.infura_secret or ''
return obj
@staticmethod
def parse(path, name, items):
member_adr = None
ethkey = None
cskey = None
username = None
email = None
network_url = None
network_realm = None
member_oid = None
vaction_oid = None
vaction_requested = None
vaction_verified = None
data_url = None
data_realm = None
infura_network = None
infura_key = None
infura_secret = None
infura_url = None
for k, v in items:
if k == 'network_url':
network_url = str(v)
elif k == 'network_realm':
network_realm = str(v)
elif k == 'vaction_oid':
if type(v) == str and v != '':
vaction_oid = uuid.UUID(v)
else:
vaction_oid = None
elif k == 'member_adr':
if type(v) == str and v != '':
member_adr = v
else:
member_adr = None
elif k == 'member_oid':
if type(v) == str and v != '':
member_oid = uuid.UUID(v)
else:
member_oid = None
elif k == 'vaction_requested':
if type(v) == int and v:
vaction_requested = np.datetime64(v, 'ns')
else:
vaction_requested = v
elif k == 'vaction_verified':
if type(v) == int:
vaction_verified = np.datetime64(v, 'ns')
else:
vaction_verified = v
elif k == 'data_url':
data_url = str(v)
elif k == 'data_realm':
data_realm = str(v)
elif k == 'ethkey':
ethkey = binascii.a2b_hex(v[2:])
elif k == 'cskey':
cskey = binascii.a2b_hex(v[2:])
elif k == 'username':
username = str(v)
elif k == 'email':
email = str(v)
elif k == 'infura_network':
infura_network = str(v)
elif k == 'infura_key':
infura_key = str(v)
elif k == 'infura_secret':
infura_secret = str(v)
elif k == 'infura_url':
infura_url = str(v)
elif k in ['path', 'name']:
pass
else:
# skip unknown attribute
print('unprocessed config attribute "{}"'.format(k))
return Profile(path, name,
member_adr, ethkey, cskey,
username, email,
network_url, network_realm,
member_oid,
vaction_oid, vaction_requested, vaction_verified,
data_url, data_realm,
infura_url, infura_network, infura_key, infura_secret)
class UserConfig(object):
"""
Local user configuration file. The data is either a plain text (unencrypted)
.ini file, or such a file encrypted with XSalsa20-Poly1305, and with a
binary file header of 48 octets.
"""
def __init__(self, config_path):
"""
:param config_path: The user configuration file path.
"""
from txaio import make_logger
self.log = make_logger()
self._config_path = os.path.abspath(config_path)
self._profiles = {}
@property
def config_path(self) -> List[str]:
"""
Return the path to the user configuration file exposed by this object.,
:return: Local filesystem path.
"""
return self._config_path
@property
def profiles(self) -> Dict[str, object]:
"""
Access to a map of user profiles in this user configuration.
:return: Map of user profiles.
"""
return self._profiles
def save(self, [PASSWORD-REMOVED][str] = None):
"""
Save this user configuration to the underlying configuration file. The user
configuration file can be encrypted using Argon2id when a ``password`` is given.
:param password: The optional Argon2id password.
:return: Number of octets written to the user configuration file.
"""
written = 0
config = configparser.ConfigParser()
for profile_name, profile in self._profiles.items():
if profile_name not in config.sections():
config.add_section(profile_name)
written += 1
pd = profile.marshal()
for option, value in pd.items():
config.set(profile_name, option, value)
with io.StringIO() as fp1:
config.write(fp1)
config_data = fp1.getvalue().encode('utf8')
if password:
# binary file format header (48 bytes):
#
# * 8 bytes: 0xdeadbeef 0x00000666 magic number (big endian)
# * 4 bytes: 0x00000001 encryption type 1 for "argon2id"
# * 4 bytes data length (big endian)
# * 8 bytes created timestamp ns (big endian)
# * 8 bytes unused (filled 0x00 currently)
# * 16 bytes salt
#
salt = os.urandom(16)
context = 'xbrnetwork-config'
priv_key = pkm_from_argon2_secret(email='', [PASSWORD-REMOVED], context=context, salt=salt)
box = nacl.secret.SecretBox(priv_key)
config_data_ciphertext = box.encrypt(config_data)
dl = [
b'\xde\xad\xbe\xef',
b'\x00\x00\x06\x66',
b'\x00\x00\x00\x01',
struct.pack('>L', len(config_data_ciphertext)),
struct.pack('>Q', time_ns()),
b'\x00' * 8,
salt,
config_data_ciphertext,
]
data = b''.join(dl)
else:
data = config_data
with open(self._config_path, 'wb') as fp2:
fp2.write(data)
self.log.debug('configuration with {sections} sections, {bytes_written} bytes written to {written_to}',
sections=written, bytes_written=len(data), written_to=self._config_path)
return len(data)
def load(self, cb_get_password=None) -> List[str]:
"""
Load this object from the underlying user configuration file. When the
file is encrypted, call back into ``cb_get_password`` to get the user password.
:param cb_get_[PASSWORD-REMOVED] called when password is needed.
:return: List of profiles loaded.
"""
if not os.path.exists(self._config_path) or not os.path.isfile(self._config_path):
raise RuntimeError('config path "{}" cannot be loaded: so such file'.format(self._config_path))
with open(self._config_path, 'rb') as fp:
data = fp.read()
if len(data) >= 48 and data[:8] == b'\xde\xad\xbe\xef\x00\x00\x06\x66':
# binary format detected
header = data[:48]
body = data[48:]
algo = struct.unpack('>L', header[8:12])[0]
data_len = struct.unpack('>L', header[12:16])[0]
created = struct.unpack('>Q', header[16:24])[0]
# created_ts = np.datetime64(created, 'ns')
assert algo in [0, 1]
assert data_len == len(body)
assert created < time_ns()
salt = header[32:48]
context = 'xbrnetwork-config'
if cb_get_password:
[PASSWORD-REMOVED]()
else:
password = ''
priv_key = pkm_from_argon2_secret(email='', [PASSWORD-REMOVED], context=context, salt=salt)
box = nacl.secret.SecretBox(priv_key)
body = box.decrypt(body)
else:
header = None
body = data
config = configparser.ConfigParser()
config.read_string(body.decode('utf8'))
profiles = {}
for profile_name in config.sections():
citems = config.items(profile_name)
profile = Profile.parse(self._config_path, profile_name, citems)
profiles[profile_name] = profile
self._profiles = profiles
loaded_profiles = sorted(self.profiles.keys())
return loaded_profiles
if 'CROSSBAR_FABRIC_URL' in os***REMOVED***iron:
_DEFAULT_CFC_URL = os***REMOVED***iron['CROSSBAR_FABRIC_URL']
else:
_DEFAULT_CFC_URL = u'wss://master.xbr.network/ws'
def style_error(text):
if _HAS_COLOR_TERM:
return click.style(text, fg='red', bold=True)
else:
return text
def style_ok(text):
if _HAS_COLOR_TERM:
return click.style(text, fg='green', bold=True)
else:
return text
class WampUrl(click.ParamType):
"""
WAMP transport URL validator.
"""
name = 'WAMP transport URL'
def __init__(self):
click.ParamType.__init__(self)
def convert(self, value, param, ctx):
try:
parse_url(value)
except Exception as e:
self.fail(style_error(str(e)))
else:
return value
def prompt_for_wamp_url(msg, default=None):
"""
Prompt user for WAMP transport URL (eg "wss://planet.xbr.network/ws").
"""
value = click.prompt(msg, type=WampUrl(), default=default)
return value
class EthereumAddress(click.ParamType):
"""
Ethereum address validator.
"""
name = 'Ethereum address'
def __init__(self):
click.ParamType.__init__(self)
def convert(self, value, param, ctx):
try:
value = web3.Web3.toChecksumAddress(value)
adr = binascii.a2b_hex(value[2:])
if len(value) != 20:
raise ValueError('Ethereum addres must be 20 bytes (160 bit), but was {} bytes'.format(len(adr)))
except Exception as e:
self.fail(style_error(str(e)))
else:
return value
def prompt_for_ethereum_address(msg):
"""
Prompt user for an Ethereum (public) address.
"""
value = click.prompt(msg, type=EthereumAddress())
return value
class PrivateKey(click.ParamType):
"""
Private key (32 bytes in HEX) validator.
"""
name = 'Private key'
def __init__(self, key_len):
click.ParamType.__init__(self)
self._key_len = key_len
def convert(self, value, param, ctx):
try:
value = hexstr_if_str(to_hex, value)
if value[:2] in ['0x', '\\x']:
key = binascii.a2b_hex(value[2:])
else:
key = binascii.a2b_hex(value)
if len(key) != self._key_len:
raise ValueError('key length must be {} bytes, but was {} bytes'.format(self._key_len, len(key)))
except Exception as e:
self.fail(style_error(str(e)))
else:
return value
def prompt_for_key(msg, key_len, default=None):
"""
Prompt user for a binary key of given length (in HEX).
"""
value = click.prompt(msg, type=PrivateKey(key_len), default=default)
return value
# default configuration stored in $HOME/.xbrnetwork/config.ini
_DEFAULT_CONFIG = """[default]
# username used with this profile
username={username}
# user email used with the profile (e.g. for verification emails)
email={email}
# XBR network node used as a directory server and gateway to XBR smart contracts
network_url={network_url}
# WAMP realm on network node, usually "xbrnetwork"
network_realm={network_realm}
# user private WAMP-cryptosign key (for client authentication)
cskey={cskey}
# user private Ethereum key (for signing transactions and e2e data encryption)
ethkey={ethkey}
"""
# # default XBR market URL to connect to
# market_url={market_url}
# market_realm={market_realm}
# # Infura blockchain gateway configuration
# infura_url={infura_url}
# infura_network={infura_network}
# infura_key={infura_key}
# infura_secret={infura_secret}
def load_or_create_profile(dotdir=None, profile=None, default_url=None, default_realm=None, default_email=None, default_username=None):
dotdir = dotdir or '~/.xbrnetwork'
profile = profile or 'default'
default_url = default_url or 'wss://planet.xbr.network/ws'
default_realm = default_realm or 'xbrnetwork'
config_dir = os.path.expanduser(dotdir)
if not os.path.isdir(config_dir):
os.mkdir(config_dir)
click.echo('created new local user directory {}'.format(style_ok(config_dir)))
config_path = os.path.join(config_dir, 'config.ini')
if not os.path.isfile(config_path):
click.echo('creating new user profile "{}"'.format(style_ok(profile)))
with open(config_path, 'w') as f:
network_url = prompt_for_wamp_url('enter the WAMP router URL of the network directory node', default=default_url)
network_realm = click.prompt('enter the WAMP realm to join on the network directory node', type=str, default=default_realm)
cskey = prompt_for_key('your private WAMP client key', 32, default='0x' + binascii.b2a_hex(os.urandom(32)).decode())
ethkey = prompt_for_key('your private Etherum key', 32, default='0x' + binascii.b2a_hex(os.urandom(32)).decode())
email = click.prompt('user email used for with profile', type=str, default=default_email)
username = click.prompt('user name used with this profile', type=str, default=default_username)
f.write(_DEFAULT_CONFIG.format(network_url=network_url, network_realm=network_realm, ethkey=ethkey,
cskey=cskey, email=email, username=username))
click.echo('created new local user configuration {}'.format(style_ok(config_path)))
config_obj = UserConfig(config_path)
config_obj.load()
profile_obj = config_obj.profiles.get(profile, None)
if not profile_obj:
raise click.ClickException('no such profile: "{}"'.format(profile))
return profile_obj

View File

@@ -0,0 +1,87 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
import shutil
from twisted.internet.defer import Deferred
from twisted.internet.interfaces import IReactorProcess
from twisted.internet.utils import getProcessOutput
__all__ = ('get_input_from_dialog',)
def get_input_from_dialog(reactor: IReactorProcess, title: str = 'Unlock key',
text: str = 'Please enter passphrase to unlock key',
hide_text: bool = True) -> Deferred:
"""
Show a Gnome/GTK desktop dialog asking for a passphrase.
This is using zenity, the GNOME port of dialog which allows you to display dialog boxes
from the commandline and shell scripts. To install (on Linux):
.. code:: console
sudo apt update
sudo install zenity
See also:
- https://gitlab.gnome.org/GNOME/zenity
- https://wiki.ubuntuusers.de/Zenity/
- https://bash.cyberciti.biz/guide/Zenity:_Shell_Scripting_with_Gnome
:param reactor: Twisted reactor to use.
:param title: Dialog window title to show.
:param text: Dialog text to show above text entry field.
:param hide_text: Hide entry field text.
:return: A deferred that resolves with the string the user entered.
"""
exe = shutil.which('zenity')
if not exe:
raise RuntimeError('get_input_from_dialog(): could not find zenity (install with "apt install zenity")')
args = ['--entry', '--title', title, '--text', text]
if hide_text:
args.append('--hide-text')
d = getProcessOutput(exe, args=args, env=os***REMOVED***iron, reactor=reactor)
def _consume(output):
passphrase = output.decode('utf8').strip()
return passphrase
d.addCallback(_consume)
return d
if __name__ == "__main__":
from twisted.internet.task import react
async def main(reactor):
passphrase = await get_input_from_dialog(reactor)
print(type(passphrase), len(passphrase), '"{}"'.format(passphrase))
react(main)

View File

@@ -0,0 +1,152 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from typing import Optional
from ._eip712_base import sign, recover, is_address, is_bytes16, is_block_number, \
is_chain_id, is_eth_privkey, is_signature
def _create_eip712_api_publish(chainId: int, verifyingContract: bytes, member: bytes, published: int,
catalogId: bytes, apiId: bytes, schema: str, meta: Optional[str]) -> dict:
"""
:param chainId:
:param verifyingContract:
:param member:
:param published:
:param catalogId:
:param apiId:
:param schema:
:param meta:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_address(member)
assert is_block_number(published)
assert is_bytes16(catalogId)
assert is_bytes16(apiId)
assert type(schema) == str
assert type(meta) == str
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712ApiPublish': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'member',
'type': 'address'
},
{
'name': 'published',
'type': 'uint256'
},
{
'name': 'catalogId',
'type': 'bytes16'
},
{
'name': 'apiId',
'type': 'bytes16'
},
{
'name': 'schema',
'type': 'string'
},
{
'name': 'meta',
'type': 'string'
},
]
},
'primaryType': 'EIP712ApiPublish',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'member': member,
'published': published,
'catalogId': catalogId,
'apiId': apiId,
'schema': schema,
'meta': meta or '',
}
}
return data
def sign_eip712_api_publish(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
published: int, catalogId: bytes, apiId: bytes, schema: str, meta: Optional[str]) -> bytes:
"""
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_api_publish(chainId, verifyingContract, member, published, catalogId, apiId, schema,
meta)
return sign(eth_privkey, data)
def recover_eip712_api_publish(chainId: int, verifyingContract: bytes, member: bytes, published: int,
catalogId: bytes, apiId: bytes, schema: str, meta: Optional[str],
signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_api_publish(chainId, verifyingContract, member, published, catalogId, apiId, schema,
meta)
return recover(data, signature)

View File

@@ -0,0 +1,462 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os.path
import pprint
from binascii import a2b_hex
from typing import Dict, Any, Optional, List
import web3
import cbor2
from py_eth_sig_utils.eip712 import encode_typed_data
from autobahn.wamp.message import _URI_PAT_REALM_NAME_ETH
from autobahn.xbr._secmod import EthereumKey
from ._eip712_base import sign, recover, is_chain_id, is_address, is_block_number, is_signature, is_eth_privkey
from ._eip712_certificate import EIP712Certificate
def create_eip712_authority_certificate(chainId: int,
verifyingContract: bytes,
validFrom: int,
issuer: bytes,
subject: bytes,
realm: bytes,
capabilities: int,
meta: str) -> dict:
"""
Authority certificate: long-lived, on-chain L2.
:param chainId:
:param verifyingContract:
:param validFrom:
:param issuer:
:param subject:
:param realm:
:param capabilities:
:param meta:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_block_number(validFrom)
assert is_address(issuer)
assert is_address(subject)
assert is_address(realm)
assert type(capabilities) == int and 0 <= capabilities <= 2 ** 53
assert meta is None or type(meta) == str
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712AuthorityCertificate': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'validFrom',
'type': 'uint256'
},
{
'name': 'issuer',
'type': 'address'
},
{
'name': 'subject',
'type': 'address'
},
{
'name': 'realm',
'type': 'address'
},
{
'name': 'capabilities',
'type': 'uint64'
},
{
'name': 'meta',
'type': 'string'
}
]
},
'primaryType': 'EIP712AuthorityCertificate',
'domain': {
'name': 'WMP',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'validFrom': validFrom,
'issuer': issuer,
'subject': subject,
'realm': realm,
'capabilities': capabilities,
'meta': meta or '',
}
}
return data
def sign_eip712_authority_certificate(eth_privkey: bytes,
chainId: int,
verifyingContract: bytes,
validFrom: int,
issuer: bytes,
subject: bytes,
realm: bytes,
capabilities: int,
meta: str) -> bytes:
"""
Sign the given data using a EIP712 based signature with the provided private key.
:param eth_privkey:
:param chainId:
:param verifyingContract:
:param validFrom:
:param issuer:
:param subject:
:param realm:
:param capabilities:
:param meta:
:return:
"""
assert is_eth_privkey(eth_privkey)
data = create_eip712_authority_certificate(chainId, verifyingContract, validFrom, issuer,
subject, realm, capabilities, meta)
return sign(eth_privkey, data)
def recover_eip712_authority_certificate(chainId: int,
verifyingContract: bytes,
validFrom: int,
issuer: bytes,
subject: bytes,
realm: bytes,
capabilities: int,
meta: str,
signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:param chainId:
:param verifyingContract:
:param validFrom:
:param issuer:
:param subject:
:param realm:
:param capabilities:
:param meta:
:param signature:
:return: The (computed) signer address the signature was signed with.
"""
assert is_signature(signature)
data = create_eip712_authority_certificate(chainId, verifyingContract, validFrom, issuer,
subject, realm, capabilities, meta)
return recover(data, signature)
class EIP712AuthorityCertificate(EIP712Certificate):
CAPABILITY_ROOT_CA = 1
CAPABILITY_INTERMEDIATE_CA = 2
CAPABILITY_PUBLIC_RELAY = 4
CAPABILITY_PRIVATE_RELAY = 8
CAPABILITY_PROVIDER = 16
CAPABILITY_CONSUMER = 32
__slots__ = (
# EIP712 attributes
'chainId',
'verifyingContract',
'validFrom',
'issuer',
'subject',
'realm',
'capabilities',
'meta',
# additional attributes
'signatures',
'hash',
)
def __init__(self, chainId: int, verifyingContract: bytes, validFrom: int, issuer: bytes, subject: bytes,
realm: bytes, capabilities: int, meta: str,
signatures: Optional[List[bytes]] = None):
super().__init__(chainId, verifyingContract, validFrom)
self.issuer = issuer
self.subject = subject
self.realm = realm
self.capabilities = capabilities
self.meta = meta
self.signatures = signatures
eip712 = create_eip712_authority_certificate(chainId,
verifyingContract,
validFrom,
issuer,
subject,
realm,
capabilities,
meta)
self.hash = encode_typed_data(eip712)
def __eq__(self, other: Any) -> bool:
if not isinstance(other, self.__class__):
return False
if not EIP712AuthorityCertificate.__eq__(self, other):
return False
if other.chainId != self.chainId:
return False
if other.verifyingContract != self.verifyingContract:
return False
if other.validFrom != self.validFrom:
return False
if other.issuer != self.issuer:
return False
if other.subject != self.subject:
return False
if other.realm != self.realm:
return False
if other.capabilities != self.capabilities:
return False
if other.meta != self.meta:
return False
if other.signatures != self.signatures:
return False
if other.hash != self.hash:
return False
return True
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __str__(self) -> str:
return pprint.pformat(self.marshal())
def sign(self, key: EthereumKey, binary: bool = False) -> bytes:
eip712 = create_eip712_authority_certificate(self.chainId,
self.verifyingContract,
self.validFrom,
self.issuer,
self.subject,
self.realm,
self.capabilities,
self.meta)
return key.sign_typed_data(eip712, binary=binary)
def recover(self, signature: bytes) -> bytes:
return recover_eip712_authority_certificate(self.chainId,
self.verifyingContract,
self.validFrom,
self.issuer,
self.subject,
self.realm,
self.capabilities,
self.meta,
signature)
def marshal(self, binary: bool = False) -> Dict[str, Any]:
obj = create_eip712_authority_certificate(chainId=self.chainId,
verifyingContract=self.verifyingContract,
validFrom=self.validFrom,
issuer=self.issuer,
subject=self.subject,
realm=self.realm,
capabilities=self.capabilities,
meta=self.meta)
if not binary:
obj['message']['verifyingContract'] = web3.Web3.toChecksumAddress(obj['message']['verifyingContract']) if obj['message']['verifyingContract'] else None
obj['message']['issuer'] = web3.Web3.toChecksumAddress(obj['message']['issuer']) if obj['message']['issuer'] else None
obj['message']['subject'] = web3.Web3.toChecksumAddress(obj['message']['subject']) if obj['message']['subject'] else None
obj['message']['realm'] = web3.Web3.toChecksumAddress(obj['message']['realm']) if obj['message']['realm'] else None
return obj
@staticmethod
def parse(obj, binary: bool = False) -> 'EIP712AuthorityCertificate':
if type(obj) != dict:
raise ValueError('invalid type {} for object in EIP712AuthorityCertificate.parse'.format(type(obj)))
primaryType = obj.get('primaryType', None)
if primaryType != 'EIP712AuthorityCertificate':
raise ValueError('invalid primaryType "{}" - expected "EIP712AuthorityCertificate"'.format(primaryType))
# FIXME: check EIP712 types, domain
data = obj.get('message', None)
if type(data) != dict:
raise ValueError('invalid type {} for EIP712AuthorityCertificate'.format(type(data)))
for k in data:
if k not in ['type', 'chainId', 'verifyingContract', 'validFrom', 'issuer', 'subject',
'realm', 'capabilities', 'meta']:
raise ValueError('invalid attribute "{}" in EIP712AuthorityCertificate'.format(k))
_type = data.get('type', None)
if _type and _type != 'EIP712AuthorityCertificate':
raise ValueError('unexpected type "{}" in EIP712AuthorityCertificate'.format(_type))
chainId = data.get('chainId', None)
if chainId is None:
raise ValueError('missing chainId in EIP712AuthorityCertificate')
if type(chainId) != int:
raise ValueError('invalid type {} for chainId in EIP712AuthorityCertificate'.format(type(chainId)))
verifyingContract = data.get('verifyingContract', None)
if verifyingContract is None:
raise ValueError('missing verifyingContract in EIP712AuthorityCertificate')
if binary:
if type(verifyingContract) != bytes:
raise ValueError(
'invalid type {} for verifyingContract in EIP712AuthorityCertificate'.format(type(verifyingContract)))
if len(verifyingContract) != 20:
raise ValueError('invalid value length {} of verifyingContract'.format(len(verifyingContract)))
else:
if type(verifyingContract) != str:
raise ValueError(
'invalid type {} for verifyingContract in EIP712AuthorityCertificate'.format(type(verifyingContract)))
if not _URI_PAT_REALM_NAME_ETH.match(verifyingContract):
raise ValueError(
'invalid value "{}" for verifyingContract in EIP712AuthorityCertificate'.format(verifyingContract))
verifyingContract = a2b_hex(verifyingContract[2:])
validFrom = data.get('validFrom', None)
if validFrom is None:
raise ValueError('missing validFrom in EIP712AuthorityCertificate')
if type(validFrom) != int:
raise ValueError('invalid type {} for validFrom in EIP712AuthorityCertificate'.format(type(validFrom)))
issuer = data.get('issuer', None)
if issuer is None:
raise ValueError('missing issuer in EIP712AuthorityCertificate')
if binary:
if type(issuer) != bytes:
raise ValueError(
'invalid type {} for issuer in EIP712AuthorityCertificate'.format(type(issuer)))
if len(issuer) != 20:
raise ValueError('invalid value length {} of issuer'.format(len(issuer)))
else:
if type(issuer) != str:
raise ValueError('invalid type {} for issuer in EIP712AuthorityCertificate'.format(type(issuer)))
if not _URI_PAT_REALM_NAME_ETH.match(issuer):
raise ValueError('invalid value "{}" for issuer in EIP712AuthorityCertificate'.format(issuer))
issuer = a2b_hex(issuer[2:])
subject = data.get('subject', None)
if subject is None:
raise ValueError('missing subject in EIP712AuthorityCertificate')
if binary:
if type(subject) != bytes:
raise ValueError(
'invalid type {} for subject in EIP712AuthorityCertificate'.format(type(subject)))
if len(subject) != 20:
raise ValueError('invalid value length {} of verifyingContract'.format(len(subject)))
else:
if type(subject) != str:
raise ValueError('invalid type {} for subject in EIP712AuthorityCertificate'.format(type(subject)))
if not _URI_PAT_REALM_NAME_ETH.match(subject):
raise ValueError('invalid value "{}" for subject in EIP712AuthorityCertificate'.format(subject))
subject = a2b_hex(subject[2:])
realm = data.get('realm', None)
if realm is None:
raise ValueError('missing realm in EIP712AuthorityCertificate')
if binary:
if type(realm) != bytes:
raise ValueError(
'invalid type {} for realm in EIP712AuthorityCertificate'.format(type(realm)))
if len(realm) != 20:
raise ValueError('invalid value length {} of realm'.format(len(realm)))
else:
if type(realm) != str:
raise ValueError('invalid type {} for realm in EIP712AuthorityCertificate'.format(type(realm)))
if not _URI_PAT_REALM_NAME_ETH.match(realm):
raise ValueError('invalid value "{}" for realm in EIP712AuthorityCertificate'.format(realm))
realm = a2b_hex(realm[2:])
capabilities = data.get('capabilities', None)
if capabilities is None:
raise ValueError('missing capabilities in EIP712AuthorityCertificate')
if type(capabilities) != int:
raise ValueError('invalid type {} for capabilities in EIP712AuthorityCertificate'.format(type(capabilities)))
meta = data.get('meta', None)
if meta is None:
raise ValueError('missing meta in EIP712AuthorityCertificate')
if type(meta) != str:
raise ValueError('invalid type {} for meta in EIP712AuthorityCertificate'.format(type(meta)))
obj = EIP712AuthorityCertificate(chainId=chainId, verifyingContract=verifyingContract, validFrom=validFrom,
issuer=issuer, subject=subject, realm=realm, capabilities=capabilities, meta=meta)
return obj
def save(self, filename: str) -> int:
"""
Save certificate to file. File format (serialized as CBOR):
[cert_hash: bytes, cert_eip712: Dict[str, Any], cert_signatures: List[bytes]]
:param filename:
:return:
"""
cert_obj = [self.hash, self.marshal(binary=True), self.signatures or []]
with open(filename, 'wb') as f:
data = cbor2.dumps(cert_obj)
f.write(data)
return len(data)
@staticmethod
def load(filename) -> 'EIP712AuthorityCertificate':
"""
Load certificate from file.
:param filename:
:return:
"""
if not os.path.isfile(filename):
raise RuntimeError('cannot create EIP712AuthorityCertificate from filename "{}": not a file'.format(filename))
with open(filename, 'rb') as f:
cert_hash, cert_eip712, cert_signatures = cbor2.loads(f.read())
cert = EIP712AuthorityCertificate.parse(cert_eip712, binary=True)
assert cert_hash == cert.hash
cert.signatures = cert_signatures
return cert

View File

@@ -0,0 +1,164 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from typing import Dict, Any
from binascii import a2b_hex
from py_eth_sig_utils import signing
_EIP712_SIG_LEN = 32 + 32 + 1
def _hash(data) -> bytes:
"""
keccak256(abi.encode(
EIP712_MEMBER_REGISTER_TYPEHASH,
obj.chainId,
obj.verifyingContract,
obj.member,
obj.registered,
keccak256(bytes(obj.eula)),
keccak256(bytes(obj.profile))
));
:param data:
:return:
"""
def sign(eth_privkey: bytes, data: Dict[str, Any]) -> bytes:
"""
Sign the given data using the given Ethereum private key.
:param eth_privkey: Signing key.
:param data: Data to sign.
:return: Signature.
"""
# internally, this is using py_eth_sig_utils.eip712.encode_typed_data
_args = signing.sign_typed_data(data, eth_privkey)
# serialize structured signature (v, r, s) into bytes
signature = signing.v_r_s_to_signature(*_args)
# be paranoid about what to expect
assert type(signature) == bytes and len(signature) == _EIP712_SIG_LEN
return signature
def recover(data: Dict[str, Any], signature: bytes) -> bytes:
"""
Recover the Ethereum address of the signer, given the data and signature.
:param data: Signed data.
:param signature: Signature.
:return: Signing address.
"""
assert type(signature) == bytes and len(signature) == _EIP712_SIG_LEN
signer_address = signing.recover_typed_data(data, *signing.signature_to_v_r_s(signature))
return a2b_hex(signer_address[2:])
def is_address(provided: Any) -> bool:
"""
Check if the value is a proper Ethereum address.
:param provided: The value to check.
:return: True iff the value is of correct type.
"""
return type(provided) == bytes and len(provided) == 20
def is_bytes16(provided: Any) -> bool:
"""
Check if the value is a proper (binary) UUID.
:param provided: The value to check.
:return: True iff the value is of correct type.
"""
return type(provided) == bytes and len(provided) == 16
def is_bytes32(provided: Any) -> bool:
"""
Check if the value is of type bytes and length 32.
:param provided: The value to check.
:return: True iff the value is of correct type.
"""
return type(provided) == bytes and len(provided) == 32
def is_signature(provided: Any) -> bool:
"""
Check if the value is a proper Ethereum signature.
:param provided: The value to check.
:return: True iff the value is of correct type.
"""
return type(provided) == bytes and len(provided) == _EIP712_SIG_LEN
def is_eth_privkey(provided: Any) -> bool:
"""
Check if the value is a proper Ethereum private key (seed).
:param provided: The value to check.
:return: True iff the value is of correct type.
"""
return type(provided) == bytes and len(provided) == 32
def is_cs_pubkey(provided: Any) -> bool:
"""
Check if the value is a proper WAMP-cryptosign public key.
:param provided: The value to check.
:return: True iff the value is of correct type.
"""
return type(provided) == bytes and len(provided) == 32
def is_block_number(provided: Any) -> bool:
"""
Check if the value is a proper Ethereum block number.
:param provided: The value to check.
:return: True iff the value is of correct type.
"""
return type(provided) == int
def is_chain_id(provided: Any) -> bool:
"""
Check if the value is a proper Ethereum chain ID.
:param provided: The value to check.
:return: True iff the value is of correct type.
"""
# here is a list of public networks: https://chainid.network/
# note: we allow any positive integer to account for private networks
return type(provided) == int and provided > 0

View File

@@ -0,0 +1,141 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from typing import Optional
from ._eip712_base import sign, recover, is_address, is_signature, is_eth_privkey, \
is_bytes16, is_chain_id
def _create_eip712_catalog_create(chainId: int, verifyingContract: bytes, member: bytes, created: int,
catalogId: bytes, terms: str, meta: Optional[str]) -> dict:
"""
:param chainId:
:param verifyingContract:
:param member:
:param created:
:param catalogId:
:param terms:
:param meta:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_address(member)
assert is_bytes16(catalogId)
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712CatalogCreate': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'member',
'type': 'address'
},
{
'name': 'created',
'type': 'uint256'
},
{
'name': 'catalogId',
'type': 'bytes16'
},
{
'name': 'terms',
'type': 'string'
},
{
'name': 'meta',
'type': 'string'
},
]
},
'primaryType': 'EIP712CatalogCreate',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'member': member,
'created': created,
'catalogId': catalogId,
'terms': terms,
'meta': meta or '',
}
}
return data
def sign_eip712_catalog_create(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
created: int, catalogId: bytes, terms: str, meta: Optional[str]) -> bytes:
"""
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_catalog_create(chainId, verifyingContract, member, created, catalogId, terms, meta)
return sign(eth_privkey, data)
def recover_eip712_catalog_create(chainId: int, verifyingContract: bytes, member: bytes, created: int,
catalogId: bytes, terms: str, meta: Optional[str], signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_catalog_create(chainId, verifyingContract, member, created, catalogId, terms, meta)
return recover(data, signature)

View File

@@ -0,0 +1,40 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
class EIP712Certificate(object):
def __init__(self, chainId: int, verifyingContract: bytes, validFrom: int):
self.chainId = chainId
self.verifyingContract = verifyingContract
self.validFrom = validFrom
def recover(self, signature: bytes) -> bytes:
raise NotImplementedError()
@staticmethod
def parse(data) -> 'EIP712Certificate':
raise NotImplementedError()

View File

@@ -0,0 +1,117 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from typing import List, Tuple, Dict, Any, Union
from autobahn.xbr._eip712_delegate_certificate import EIP712DelegateCertificate
from autobahn.xbr._eip712_authority_certificate import EIP712AuthorityCertificate
def parse_certificate_chain(certificates: List[Tuple[Dict[str, Any], str]]) \
-> List[Union[EIP712DelegateCertificate, EIP712AuthorityCertificate]]:
"""
:param certificates:
:return:
"""
# parse the whole certificate chain
cert_chain = []
for cert_hash, cert_data, cert_sig in certificates:
if cert_data['primaryType'] == 'EIP712DelegateCertificate':
cert = EIP712DelegateCertificate.parse(cert_data)
elif cert_data['primaryType'] == 'EIP712AuthorityCertificate':
cert = EIP712AuthorityCertificate.parse(cert_data)
else:
assert False, 'should not arrive here'
cert_chain.append(cert)
# FIXME: proper adaptive implementation of certificate chain rules checking
# if False:
# assert len(cert_chain) == 3
#
# # Certificate Chain Rules (CCR):
# #
# # 1. **CCR-1**: The `chainId` and `verifyingContract` must match for all certificates to what we expect, and `validFrom` before current block number on the respective chain.
# # 2. **CCR-2**: The `realm` must match for all certificates to the respective realm.
# # 3. **CCR-3**: The type of the first certificate in the chain must be a `EIP712DelegateCertificate`, and all subsequent certificates must be of type `EIP712AuthorityCertificate`.
# # 4. **CCR-4**: The last certificate must be self-signed (`issuer` equals `subject`), it is a root CA certificate.
# # 5. **CCR-5**: The intermediate certificate's `issuer` must be equal to the `subject` of the previous certificate.
# # 6. **CCR-6**: The root certificate must be `validFrom` before the intermediate certificate
# # 7. **CCR-7**: The `capabilities` of intermediate certificate must be a subset of the root cert
# # 8. **CCR-8**: The intermediate certificate's `subject` must be the delegate certificate `delegate`
# # 9. **CCR-9**: The intermediate certificate must be `validFrom` before the delegate certificate
# # 10. **CCR-10**: The root certificate's signature must be valid and signed by the root certificate's `issuer`.
# # 11. **CCR-11**: The intermediate certificate's signature must be valid and signed by the intermediate certificate's `issuer`.
# # 12. **CCR-12**: The delegate certificate's signature must be valid and signed by the `delegate`.
#
# # CCR-1
# chainId = 1
# verifyingContract = a2b_hex('[AWS-SECRET-REMOVED]57'[2:])
# for cert in cert_chain:
# assert cert.chainId == chainId
# assert cert.verifyingContract == verifyingContract
#
# # CCR-2
# realm = a2b_hex('[AWS-SECRET-REMOVED]96'[2:])
# for cert in cert_chain[1:]:
# assert cert.realm == realm
#
# # CCR-3
# assert isinstance(cert_chain[0], EIP712DelegateCertificate)
# for i in [1, len(cert_chain) - 1]:
# assert isinstance(cert_chain[i], EIP712AuthorityCertificate)
#
# # CCR-4
# assert cert_chain[2].subject == cert_chain[2].issuer
#
# # CCR-5
# assert cert_chain[1].issuer == cert_chain[2].subject
#
# # CCR-6
# assert cert_chain[2].validFrom <= cert_chain[1].validFrom
#
# # CCR-7
# assert cert_chain[2].capabilities == cert_chain[2].capabilities | cert_chain[1].capabilities
#
# # CCR-8
# assert cert_chain[1].subject == cert_chain[0].delegate
#
# # CCR-9
# assert cert_chain[1].validFrom <= cert_chain[0].validFrom
#
# # CCR-10
# _issuer = cert_chain[2].recover(a2b_hex(cert_sigs[2]))
# assert _issuer == cert_chain[2].issuer
#
# # CCR-11
# _issuer = cert_chain[1].recover(a2b_hex(cert_sigs[1]))
# assert _issuer == cert_chain[1].issuer
#
# # CCR-12
# _issuer = cert_chain[0].recover(a2b_hex(cert_sigs[0]))
# assert _issuer == cert_chain[0].delegate
return cert_chain

View File

@@ -0,0 +1,140 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from ._eip712_base import sign, recover, is_address, is_signature, is_eth_privkey, is_bytes16, \
is_block_number, is_chain_id
def _create_eip712_channel_close(chainId: int, verifyingContract: bytes, closeAt: int, marketId: bytes, channelId: bytes,
channelSeq: int, balance: int, isFinal: bool) -> dict:
"""
:param chainId:
:param verifyingContract:
:param marketId:
:param channelId:
:param channelSeq:
:param balance:
:param isFinal:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_block_number(closeAt)
assert is_bytes16(marketId)
assert is_bytes16(channelId)
assert type(channelSeq) == int
assert type(balance) == int
assert type(isFinal) == bool
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712ChannelClose': [{
'name': 'chainId',
'type': 'uint256'
}, {
'name': 'verifyingContract',
'type': 'address'
}, {
'name': 'closeAt',
'type': 'uint256'
}, {
'name': 'marketId',
'type': 'bytes16'
}, {
'name': 'channelId',
'type': 'bytes16'
}, {
'name': 'channelSeq',
'type': 'uint32'
}, {
'name': 'balance',
'type': 'uint256'
}, {
'name': 'isFinal',
'type': 'bool'
}]
},
'primaryType': 'EIP712ChannelClose',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'closeAt': closeAt,
'marketId': marketId,
'channelId': channelId,
'channelSeq': channelSeq,
'balance': balance,
'isFinal': isFinal
}
}
return data
def sign_eip712_channel_close(eth_privkey: bytes, chainId: int, verifyingContract: bytes, closeAt: int, marketId: bytes,
channelId: bytes, channelSeq: int, balance: int, isFinal: bool) -> bytes:
"""
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_channel_close(chainId, verifyingContract, closeAt, marketId, channelId, channelSeq, balance,
isFinal)
return sign(eth_privkey, data)
def recover_eip712_channel_close(chainId: int, verifyingContract: bytes, closeAt: int, marketId: bytes, channelId: bytes,
channelSeq: int, balance: int, isFinal: bool, signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_channel_close(chainId, verifyingContract, closeAt, marketId, channelId, channelSeq, balance,
isFinal)
return recover(data, signature)

View File

@@ -0,0 +1,162 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from ._eip712_base import sign, recover, is_address, is_signature, is_eth_privkey, is_bytes16, \
is_block_number, is_chain_id
def _create_eip712_channel_open(chainId: int, verifyingContract: bytes, ctype: int, openedAt: int,
marketId: bytes, channelId: bytes, actor: bytes, delegate: bytes,
marketmaker: bytes, recipient: bytes, amount: int) -> dict:
"""
:param chainId:
:param verifyingContract:
:param ctype:
:param openedAt:
:param marketId:
:param channelId:
:param actor:
:param delegate:
:param marketmaker:
:param recipient:
:param amount:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert type(ctype) == int
assert is_block_number(openedAt)
assert is_bytes16(marketId)
assert is_bytes16(channelId)
assert is_address(actor)
assert is_address(delegate)
assert is_address(marketmaker)
assert is_address(recipient)
assert type(amount) == int
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712ChannelOpen': [{
'name': 'chainId',
'type': 'uint256'
}, {
'name': 'verifyingContract',
'type': 'address'
}, {
'name': 'ctype',
'type': 'uint8'
}, {
'name': 'openedAt',
'type': 'uint256'
}, {
'name': 'marketId',
'type': 'bytes16'
}, {
'name': 'channelId',
'type': 'bytes16'
}, {
'name': 'actor',
'type': 'address'
}, {
'name': 'delegate',
'type': 'address'
}, {
'name': 'marketmaker',
'type': 'address'
}, {
'name': 'recipient',
'type': 'address'
}, {
'name': 'amount',
'type': 'uint256'
}]
},
'primaryType': 'EIP712ChannelOpen',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'ctype': ctype,
'openedAt': openedAt,
'marketId': marketId,
'channelId': channelId,
'actor': actor,
'delegate': delegate,
'marketmaker': marketmaker,
'recipient': recipient,
'amount': amount
}
}
return data
def sign_eip712_channel_open(eth_privkey: bytes, chainId: int, verifyingContract: bytes, ctype: int,
openedAt: int, marketId: bytes, channelId: bytes, actor: bytes, delegate: bytes,
marketmaker: bytes, recipient: bytes, amount: int) -> bytes:
"""
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_channel_open(chainId, verifyingContract, ctype, openedAt, marketId, channelId,
actor, delegate, marketmaker, recipient, amount)
return sign(eth_privkey, data)
def recover_eip712_channel_open(chainId: int, verifyingContract: bytes, ctype: int, openedAt: int,
marketId: bytes, channelId: bytes, actor: bytes, delegate: bytes,
marketmaker: bytes, recipient: bytes, amount: int, signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_channel_open(chainId, verifyingContract, ctype, openedAt, marketId, channelId,
actor, delegate, marketmaker, recipient, amount)
return recover(data, signature)

View File

@@ -0,0 +1,168 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from typing import Optional
from ._eip712_base import sign, recover, is_address, is_bytes16, is_eth_privkey, \
is_signature, is_chain_id, is_block_number
def _create_eip712_consent(chainId: int, verifyingContract: bytes, member: bytes, updated: int,
marketId: bytes, delegate: bytes, delegateType: int, apiCatalog: bytes,
consent: bool, servicePrefix: Optional[str]) -> dict:
"""
:param chainId:
:param verifyingContract:
:param member:
:param updated:
:param marketId:
:param delegate:
:param delegateType:
:param apiCatalog:
:param consent:
:param servicePrefix:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_address(member)
assert is_block_number(updated)
assert is_bytes16(marketId)
assert is_address(delegate)
assert type(delegateType) == int
assert is_bytes16(apiCatalog)
assert type(consent) == bool
assert servicePrefix is None or type(servicePrefix) == str
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712Consent': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'member',
'type': 'address'
},
{
'name': 'updated',
'type': 'uint256'
},
{
'name': 'marketId',
'type': 'bytes16'
},
{
'name': 'delegate',
'type': 'address'
},
{
'name': 'delegateType',
'type': 'uint8'
},
{
'name': 'apiCatalog',
'type': 'bytes16'
},
{
'name': 'consent',
'type': 'bool'
},
{
'name': 'servicePrefix',
'type': 'string'
},
]
},
'primaryType': 'EIP712Consent',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'member': member,
'updated': updated,
'marketId': marketId,
'delegate': delegate,
'delegateType': delegateType,
'apiCatalog': apiCatalog,
'consent': consent,
'servicePrefix': servicePrefix or ''
}
}
return data
def sign_eip712_consent(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
updated: int, marketId: bytes, delegate: bytes, delegateType: int, apiCatalog: bytes,
consent: bool, servicePrefix: str) -> bytes:
"""
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_consent(chainId, verifyingContract, member, updated, marketId, delegate,
delegateType, apiCatalog, consent, servicePrefix)
return sign(eth_privkey, data)
def recover_eip712_consent(chainId: int, verifyingContract: bytes, member: bytes, updated: int,
marketId: bytes, delegate: bytes, delegateType: int, apiCatalog: bytes,
consent: bool, servicePrefix: str, signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_consent(chainId, verifyingContract, member, updated, marketId, delegate,
delegateType, apiCatalog, consent, servicePrefix)
return recover(data, signature)

View File

@@ -0,0 +1,391 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
import pprint
from typing import Dict, Any, Optional, List
from binascii import a2b_hex, b2a_hex
import web3
import cbor2
from py_eth_sig_utils.eip712 import encode_typed_data
from autobahn.wamp.message import _URI_PAT_REALM_NAME_ETH
from autobahn.xbr._secmod import EthereumKey
from ._eip712_base import sign, recover, is_chain_id, is_address, is_cs_pubkey, \
is_block_number, is_signature, is_eth_privkey
from ._eip712_certificate import EIP712Certificate
def create_eip712_delegate_certificate(chainId: int,
verifyingContract: bytes,
validFrom: int,
delegate: bytes,
csPubKey: bytes,
bootedAt: int,
meta: str) -> dict:
"""
Delegate certificate: dynamic/one-time, off-chain.
:param chainId:
:param verifyingContract:
:param validFrom:
:param delegate:
:param csPubKey:
:param bootedAt:
:param meta:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_block_number(validFrom)
assert is_address(delegate)
assert is_cs_pubkey(csPubKey)
assert type(bootedAt) == int
assert meta is None or type(meta) == str
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712DelegateCertificate': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'validFrom',
'type': 'uint256'
},
{
'name': 'delegate',
'type': 'address'
},
{
'name': 'csPubKey',
'type': 'bytes32'
},
{
'name': 'bootedAt',
'type': 'uint64'
},
{
'name': 'meta',
'type': 'string'
}
]
},
'primaryType': 'EIP712DelegateCertificate',
'domain': {
'name': 'WMP',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'validFrom': validFrom,
'delegate': delegate,
'csPubKey': csPubKey,
'bootedAt': bootedAt,
'meta': meta or '',
}
}
return data
def sign_eip712_delegate_certificate(eth_privkey: bytes,
chainId: int,
verifyingContract: bytes,
validFrom: int,
delegate: bytes,
csPubKey: bytes,
bootedAt: int,
meta: str) -> bytes:
"""
Sign the given data using a EIP712 based signature with the provided private key.
:param eth_privkey: Signing key.
:param chainId:
:param verifyingContract:
:param validFrom:
:param delegate:
:param csPubKey:
:param bootedAt:
:param meta:
:return: The signature according to EIP712 (32+32+1 raw bytes).
"""
assert is_eth_privkey(eth_privkey)
data = create_eip712_delegate_certificate(chainId, verifyingContract, validFrom, delegate,
csPubKey, bootedAt, meta)
return sign(eth_privkey, data)
def recover_eip712_delegate_certificate(chainId: int,
verifyingContract: bytes,
validFrom: int,
delegate: bytes,
csPubKey: bytes,
bootedAt: int,
meta: str,
signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:param chainId:
:param verifyingContract:
:param validFrom:
:param delegate:
:param csPubKey:
:param bootedAt:
:param signature:
:param meta:
:return: The (computed) signer address the signature was signed with.
"""
assert is_signature(signature)
data = create_eip712_delegate_certificate(chainId, verifyingContract, validFrom, delegate,
csPubKey, bootedAt, meta)
return recover(data, signature)
class EIP712DelegateCertificate(EIP712Certificate):
__slots__ = (
# EIP712 attributes
'chainId',
'verifyingContract',
'validFrom',
'delegate',
'csPubKey',
'bootedAt',
'meta',
# additional attributes
'signatures',
'hash',
)
def __init__(self, chainId: int, verifyingContract: bytes, validFrom: int, delegate: bytes, csPubKey: bytes,
bootedAt: int, meta: str, signatures: Optional[List[bytes]] = None):
super().__init__(chainId, verifyingContract, validFrom)
self.delegate = delegate
self.csPubKey = csPubKey
self.bootedAt = bootedAt
self.meta = meta
self.signatures = signatures
eip712 = create_eip712_delegate_certificate(chainId,
verifyingContract,
validFrom,
delegate,
csPubKey,
bootedAt,
meta)
self.hash = encode_typed_data(eip712)
def __eq__(self, other: Any) -> bool:
if not isinstance(other, self.__class__):
return False
if not EIP712DelegateCertificate.__eq__(self, other):
return False
if other.chainId != self.chainId:
return False
if other.verifyingContract != self.verifyingContract:
return False
if other.validFrom != self.validFrom:
return False
if other.delegate != self.delegate:
return False
if other.csPubKey != self.csPubKey:
return False
if other.bootedAt != self.bootedAt:
return False
if other.meta != self.meta:
return False
if other.signatures != self.signatures:
return False
if other.hash != self.hash:
return False
return True
def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)
def __str__(self) -> str:
return pprint.pformat(self.marshal())
def sign(self, key: EthereumKey, binary: bool = False) -> bytes:
eip712 = create_eip712_delegate_certificate(self.chainId,
self.verifyingContract,
self.validFrom,
self.delegate,
self.csPubKey,
self.bootedAt,
self.meta)
return key.sign_typed_data(eip712, binary=binary)
def recover(self, signature: bytes) -> bytes:
return recover_eip712_delegate_certificate(self.chainId,
self.verifyingContract,
self.validFrom,
self.delegate,
self.csPubKey,
self.bootedAt,
self.meta,
signature)
def marshal(self, binary: bool = False) -> Dict[str, Any]:
obj = create_eip712_delegate_certificate(chainId=self.chainId,
verifyingContract=self.verifyingContract,
validFrom=self.validFrom,
delegate=self.delegate,
csPubKey=self.csPubKey,
bootedAt=self.bootedAt,
meta=self.meta)
if not binary:
obj['message']['verifyingContract'] = web3.Web3.toChecksumAddress(obj['message']['verifyingContract']) if obj['message']['verifyingContract'] else None
obj['message']['delegate'] = web3.Web3.toChecksumAddress(obj['message']['delegate']) if obj['message']['delegate'] else None
obj['message']['csPubKey'] = b2a_hex(obj['message']['csPubKey']).decode() if obj['message']['csPubKey'] else None
return obj
@staticmethod
def parse(obj) -> 'EIP712DelegateCertificate':
if type(obj) != dict:
raise ValueError('invalid type {} for object in EIP712DelegateCertificate.parse'.format(type(obj)))
primaryType = obj.get('primaryType', None)
if primaryType != 'EIP712DelegateCertificate':
raise ValueError('invalid primaryType "{}" - expected "EIP712DelegateCertificate"'.format(primaryType))
# FIXME: check EIP712 types, domain
data = obj.get('message', None)
if type(data) != dict:
raise ValueError('invalid type {} for EIP712DelegateCertificate'.format(type(data)))
for k in data:
if k not in ['type', 'chainId', 'verifyingContract', 'delegate', 'validFrom', 'csPubKey', 'bootedAt', 'meta']:
raise ValueError('invalid attribute "{}" in EIP712DelegateCertificate'.format(k))
_type = data.get('type', None)
if _type and _type != 'EIP712DelegateCertificate':
raise ValueError('unexpected type "{}" in EIP712DelegateCertificate'.format(_type))
chainId = data.get('chainId', None)
if chainId is None:
raise ValueError('missing chainId in EIP712DelegateCertificate')
if type(chainId) != int:
raise ValueError('invalid type {} for chainId in EIP712DelegateCertificate'.format(type(chainId)))
verifyingContract = data.get('verifyingContract', None)
if verifyingContract is None:
raise ValueError('missing verifyingContract in EIP712DelegateCertificate')
if type(verifyingContract) != str:
raise ValueError(
'invalid type {} for verifyingContract in EIP712DelegateCertificate'.format(type(verifyingContract)))
if not _URI_PAT_REALM_NAME_ETH.match(verifyingContract):
raise ValueError(
'invalid value "{}" for verifyingContract in EIP712DelegateCertificate'.format(verifyingContract))
verifyingContract = a2b_hex(verifyingContract[2:])
validFrom = data.get('validFrom', None)
if validFrom is None:
raise ValueError('missing validFrom in EIP712DelegateCertificate')
if type(validFrom) != int:
raise ValueError('invalid type {} for validFrom in EIP712DelegateCertificate'.format(type(validFrom)))
delegate = data.get('delegate', None)
if delegate is None:
raise ValueError('missing delegate in EIP712DelegateCertificate')
if type(delegate) != str:
raise ValueError('invalid type {} for delegate in EIP712DelegateCertificate'.format(type(delegate)))
if not _URI_PAT_REALM_NAME_ETH.match(delegate):
raise ValueError('invalid value "{}" for verifyingContract in EIP712DelegateCertificate'.format(delegate))
delegate = a2b_hex(delegate[2:])
csPubKey = data.get('csPubKey', None)
if csPubKey is None:
raise ValueError('missing csPubKey in EIP712DelegateCertificate')
if type(csPubKey) != str:
raise ValueError('invalid type {} for csPubKey in EIP712DelegateCertificate'.format(type(csPubKey)))
if len(csPubKey) != 64:
raise ValueError('invalid value "{}" for csPubKey in EIP712DelegateCertificate'.format(csPubKey))
csPubKey = a2b_hex(csPubKey)
bootedAt = data.get('bootedAt', None)
if bootedAt is None:
raise ValueError('missing bootedAt in EIP712DelegateCertificate')
if type(bootedAt) != int:
raise ValueError('invalid type {} for bootedAt in EIP712DelegateCertificate'.format(type(bootedAt)))
meta = data.get('meta', None)
if meta is None:
raise ValueError('missing meta in EIP712DelegateCertificate')
if type(meta) != str:
raise ValueError('invalid type {} for meta in EIP712DelegateCertificate'.format(type(meta)))
obj = EIP712DelegateCertificate(chainId=chainId, verifyingContract=verifyingContract, validFrom=validFrom,
delegate=delegate, csPubKey=csPubKey, bootedAt=bootedAt, meta=meta)
return obj
def save(self, filename: str) -> int:
"""
Save certificate to file. File format (serialized as CBOR):
[cert_hash: bytes, cert_eip712: Dict[str, Any], cert_signatures: List[bytes]]
:param filename:
:return:
"""
cert_obj = [self.hash, self.marshal(binary=True), self.signatures or []]
with open(filename, 'wb') as f:
data = cbor2.dumps(cert_obj)
f.write(data)
return len(data)
@staticmethod
def load(filename) -> 'EIP712DelegateCertificate':
if not os.path.isfile(filename):
raise RuntimeError('cannot create EIP712DelegateCertificate from filename "{}": not a file'.format(filename))
with open(filename, 'rb') as f:
cert_hash, cert_eip712, cert_signatures = cbor2.loads(f.read())
cert = EIP712DelegateCertificate.parse(cert_eip712, binary=True)
assert cert_hash == cert.hash
cert.signatures = cert_signatures
return cert

View File

@@ -0,0 +1,157 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from typing import Optional
from ._eip712_base import sign, recover, is_chain_id, is_address, is_bytes16, is_cs_pubkey, \
is_block_number, is_signature, is_eth_privkey
def _create_eip712_domain_create(chainId: int, verifyingContract: bytes, member: bytes, created: int,
domainId: bytes, domainKey: bytes, license: str, terms: str,
meta: Optional[str]) -> dict:
"""
:param chainId:
:param verifyingContract:
:param member:
:param created:
:param domainId:
:param domainKey:
:param license:
:param terms:
:param meta:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_address(member)
assert is_block_number(created)
assert is_bytes16(domainId)
assert is_cs_pubkey(domainKey)
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712DomainCreate': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'member',
'type': 'address'
},
{
'name': 'created',
'type': 'uint256'
},
{
'name': 'domainId',
'type': 'bytes16'
},
{
'name': 'domainKey',
'type': 'bytes32'
},
{
'name': 'license',
'type': 'string'
},
{
'name': 'terms',
'type': 'string'
},
{
'name': 'meta',
'type': 'string'
},
]
},
'primaryType': 'EIP712DomainCreate',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'member': member,
'created': created,
'domainId': domainId,
'domainKey': domainKey,
'license': license,
'terms': terms,
'meta': meta or '',
}
}
return data
def sign_eip712_domain_create(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes, created: int,
domainId: bytes, domainKey: bytes, license: str, terms: str,
meta: str) -> bytes:
"""
:param eth_privkey: Ethereum address of member (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_domain_create(chainId, verifyingContract, member, created, domainId, domainKey, license,
terms, meta)
return sign(eth_privkey, data)
def recover_eip712_domain_create(chainId: int, verifyingContract: bytes, member: bytes, created: int, domainId: bytes,
domainKey: bytes, license: str, terms: str, meta: str, signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_domain_create(chainId, verifyingContract, member, created, domainId, domainKey, license,
terms, meta)
return recover(data, signature)

View File

@@ -0,0 +1,165 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from typing import Optional
from ._eip712_base import sign, recover, is_address, is_bytes16, is_block_number, \
is_chain_id, is_eth_privkey, is_signature
def _create_eip712_market_create(chainId: int, verifyingContract: bytes, member: bytes, created: int,
marketId: bytes, coin: bytes, terms: str, meta: Optional[str], maker: bytes,
providerSecurity: int, consumerSecurity: int, marketFee: int) -> dict:
"""
:param chainId:
:param verifyingContract:
:param member:
:param created:
:param marketId:
:param coin:
:param terms:
:param meta:
:param maker:
:param providerSecurity:
:param consumerSecurity:
:param marketFee:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_address(member)
assert is_block_number(created)
assert is_bytes16(marketId)
assert is_address(coin)
assert type(terms) == str
assert meta is None or type(meta) == str
assert is_address(maker)
assert type(providerSecurity) == int
assert type(consumerSecurity) == int
assert type(marketFee) == int
# FIXME: add "coin" in below once we have done that in XBRTypes
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712MarketCreate': [{
'name': 'chainId',
'type': 'uint256'
}, {
'name': 'verifyingContract',
'type': 'address'
}, {
'name': 'member',
'type': 'address'
}, {
'name': 'created',
'type': 'uint256'
}, {
'name': 'marketId',
'type': 'bytes16'
}, {
'name': 'coin',
'type': 'address'
}, {
'name': 'terms',
'type': 'string'
}, {
'name': 'meta',
'type': 'string'
}, {
'name': 'maker',
'type': 'address'
}, {
'name': 'marketFee',
'type': 'uint256',
}]
},
'primaryType': 'EIP712MarketCreate',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'member': member,
'created': created,
'marketId': marketId,
'coin': coin,
'terms': terms,
'meta': meta or '',
'maker': maker,
'marketFee': marketFee,
}
}
return data
def sign_eip712_market_create(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
created: int, marketId: bytes, coin: bytes, terms: str, meta: str, maker: bytes,
providerSecurity: int, consumerSecurity: int, marketFee: int) -> bytes:
"""
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_market_create(chainId, verifyingContract, member, created, marketId, coin, terms,
meta, maker, providerSecurity, consumerSecurity, marketFee)
return sign(eth_privkey, data)
def recover_eip712_market_create(chainId: int, verifyingContract: bytes, member: bytes, created: int,
marketId: bytes, coin: bytes, terms: str, meta: str, maker: bytes,
providerSecurity: int, consumerSecurity: int, marketFee: int,
signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_market_create(chainId, verifyingContract, member, created, marketId, coin, terms,
meta, maker, providerSecurity, consumerSecurity, marketFee)
return recover(data, signature)

View File

@@ -0,0 +1,144 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from typing import Optional
from ._eip712_base import sign, recover, is_address, is_bytes16, is_block_number, \
is_chain_id, is_eth_privkey, is_signature
def _create_eip712_market_join(chainId: int, verifyingContract: bytes, member: bytes, joined: int,
marketId: bytes, actorType: int, meta: Optional[str]) -> dict:
"""
:param chainId:
:param verifyingContract:
:param member:
:param joined:
:param marketId:
:param actorType:
:param meta:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_address(member)
assert is_block_number(joined)
assert is_bytes16(marketId)
assert type(actorType) == int
assert meta is None or type(meta) == str
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712MarketJoin': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'member',
'type': 'address'
},
{
'name': 'joined',
'type': 'uint256'
},
{
'name': 'marketId',
'type': 'bytes16'
},
{
'name': 'actorType',
'type': 'uint8'
},
{
'name': 'meta',
'type': 'string',
},
]
},
'primaryType': 'EIP712MarketJoin',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'member': member,
'joined': joined,
'marketId': marketId,
'actorType': actorType,
'meta': meta or '',
}
}
return data
def sign_eip712_market_join(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
joined: int, marketId: bytes, actorType: int, meta: str) -> bytes:
"""
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_market_join(chainId, verifyingContract, member, joined, marketId, actorType, meta)
return sign(eth_privkey, data)
def recover_eip712_market_join(chainId: int, verifyingContract: bytes, member: bytes, joined: int,
marketId: bytes, actorType: int, meta: str, signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_market_join(chainId, verifyingContract, member, joined, marketId, actorType, meta)
return recover(data, signature)

View File

@@ -0,0 +1,137 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from ._eip712_base import sign, recover, is_address, is_bytes16, is_block_number, \
is_chain_id, is_eth_privkey, is_signature
def _create_eip712_market_leave(chainId: int, verifyingContract: bytes, member: bytes, left: int,
marketId: bytes, actorType: int) -> dict:
"""
:param chainId:
:param verifyingContract:
:param member:
:param joined:
:param marketId:
:param actorType:
:param meta:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_address(member)
assert is_block_number(left)
assert is_bytes16(marketId)
assert type(actorType) == int
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712MarketLeave': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'member',
'type': 'address'
},
{
'name': 'left',
'type': 'uint256'
},
{
'name': 'marketId',
'type': 'bytes16'
},
{
'name': 'actorType',
'type': 'uint8'
},
]
},
'primaryType': 'EIP712MarketLeave',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'member': member,
'left': left,
'marketId': marketId,
'actorType': actorType,
}
}
return data
def sign_eip712_market_leave(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes, left: int,
marketId: bytes, actorType: int) -> bytes:
"""
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_market_leave(chainId, verifyingContract, member, left, marketId, actorType)
return sign(eth_privkey, data)
def recover_eip712_market_leave(chainId: int, verifyingContract: bytes, member: bytes, left: int,
marketId: bytes, actorType: int, signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_market_leave(chainId, verifyingContract, member, left, marketId, actorType)
return recover(data, signature)

View File

@@ -0,0 +1,85 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from ._eip712_base import sign, recover, is_address, is_eth_privkey, \
is_signature, is_cs_pubkey
def _create_eip712_market_member_login(member: bytes, client_pubkey: bytes) -> dict:
assert is_address(member)
assert is_cs_pubkey(client_pubkey)
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712MarketMemberLogin': [
{
'name': 'member',
'type': 'address'
},
{
'name': 'client_pubkey',
'type': 'bytes32',
},
]
},
'primaryType': 'EIP712MarketMemberLogin',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'member': member,
'client_pubkey': client_pubkey,
}
}
return data
def sign_eip712_market_member_login(eth_privkey: bytes, member: bytes, client_pubkey: bytes) -> bytes:
assert is_eth_privkey(eth_privkey)
data = _create_eip712_market_member_login(member, client_pubkey)
return sign(eth_privkey, data)
def recover_eip712_market_member_login(member: bytes, client_pubkey: bytes, signature: bytes) -> bytes:
assert is_signature(signature)
data = _create_eip712_market_member_login(member, client_pubkey)
return recover(data, signature)

View File

@@ -0,0 +1,146 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from ._eip712_base import sign, recover, is_address, is_block_number, \
is_chain_id, is_eth_privkey, is_signature, is_cs_pubkey
def _create_eip712_member_login(chainId: int, verifyingContract: bytes, member: bytes, loggedIn: int,
timestamp: int, member_email: str, client_pubkey: bytes) -> dict:
"""
:param chainId:
:param blockNumber:
:param verifyingContract:
:param member:
:param timestamp:
:param member_email:
:param client_pubkey:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_address(member)
assert is_block_number(loggedIn)
assert type(timestamp) == int
assert type(member_email) == str
assert is_cs_pubkey(client_pubkey)
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712MemberLogin': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'member',
'type': 'address'
},
{
'name': 'loggedIn',
'type': 'uint256'
},
{
'name': 'timestamp',
'type': 'uint64'
},
{
'name': 'member_email',
'type': 'string'
},
{
'name': 'client_pubkey',
'type': 'bytes32',
},
]
},
'primaryType': 'EIP712MemberLogin',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'member': member,
'loggedIn': loggedIn,
'timestamp': timestamp,
'member_email': member_email,
'client_pubkey': client_pubkey,
}
}
return data
def sign_eip712_member_login(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
loggedIn: int, timestamp: int, member_email: str, client_pubkey: bytes) -> bytes:
"""
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_member_login(chainId, verifyingContract, member, loggedIn, timestamp, member_email,
client_pubkey)
return sign(eth_privkey, data)
def recover_eip712_member_login(chainId: int, verifyingContract: bytes, member: bytes, loggedIn: int,
timestamp: int, member_email: str, client_pubkey: bytes,
signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_member_login(chainId, verifyingContract, member, loggedIn, timestamp, member_email,
client_pubkey)
return recover(data, signature)

View File

@@ -0,0 +1,137 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from typing import Optional
from ._eip712_base import sign, recover, is_address, is_block_number, \
is_chain_id, is_eth_privkey, is_signature
def _create_eip712_member_register(chainId: int, verifyingContract: bytes, member: bytes, registered: int,
eula: str, profile: Optional[str]) -> dict:
"""
:param chainId:
:param verifyingContract:
:param member:
:param registered:
:param eula:
:param profile:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_address(member)
assert is_block_number(registered)
assert type(eula) == str
assert profile is None or type(profile) == str
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712MemberRegister': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'member',
'type': 'address'
},
{
'name': 'registered',
'type': 'uint256'
},
{
'name': 'eula',
'type': 'string'
},
{
'name': 'profile',
'type': 'string'
},
]
},
'primaryType': 'EIP712MemberRegister',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'member': member,
'registered': registered,
'eula': eula,
'profile': profile or '',
}
}
return data
def sign_eip712_member_register(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
registered: int, eula: Optional[str], profile: str) -> bytes:
"""
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_member_register(chainId, verifyingContract, member, registered, eula, profile)
return sign(eth_privkey, data)
def recover_eip712_member_register(chainId: int, verifyingContract: bytes, member: bytes, registered: int,
eula: str, profile: Optional[str], signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_member_register(chainId, verifyingContract, member, registered, eula, profile)
return recover(data, signature)

View File

@@ -0,0 +1,122 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from ._eip712_base import sign, recover, is_chain_id, is_address, \
is_block_number, is_signature, is_eth_privkey
def _create_eip712_member_unregister(chainId: int, verifyingContract: bytes, member: bytes,
retired: int) -> dict:
"""
:param chainId:
:param verifyingContract:
:param member:
:param retired:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_address(member)
assert is_block_number(retired)
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712MemberUnregister': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'member',
'type': 'address'
},
{
'name': 'retired',
'type': 'uint256'
},
]
},
'primaryType': 'EIP712MemberUnregister',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'member': member,
'paired': retired,
}
}
return data
def sign_eip712_member_unregister(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
retired: int) -> bytes:
"""
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_member_unregister(chainId, verifyingContract, member, retired)
return sign(eth_privkey, data)
def recover_eip712_member_unregister(chainId: int, verifyingContract: bytes, member: bytes, retired: int,
signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_member_unregister(chainId, verifyingContract, member, retired)
return recover(data, signature)

View File

@@ -0,0 +1,167 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from typing import Optional
from ._eip712_base import sign, recover, is_chain_id, is_address, is_bytes16, is_cs_pubkey, \
is_block_number, is_signature, is_eth_privkey
def _create_eip712_node_pair(chainId: int, verifyingContract: bytes, member: bytes, paired: int,
nodeId: bytes, domainId: bytes, nodeType: int, nodeKey: bytes,
amount: int, config: Optional[str]) -> dict:
"""
:param chainId:
:param verifyingContract:
:param member:
:param paired:
:param nodeId:
:param domainId:
:param nodeKey:
:param amount:
:param config:
:return:
"""
assert is_chain_id(chainId)
assert is_address(verifyingContract)
assert is_address(member)
assert is_block_number(paired)
assert is_bytes16(nodeId)
assert is_bytes16(domainId)
assert type(nodeType) == int
assert is_cs_pubkey(nodeKey)
assert type(amount) == int
assert config is None or type(config) == str
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712NodePair': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'member',
'type': 'address'
},
{
'name': 'paired',
'type': 'uint256'
},
{
'name': 'nodeId',
'type': 'bytes16'
},
{
'name': 'domainId',
'type': 'bytes16'
},
{
'name': 'nodeType',
'type': 'uint8'
},
{
'name': 'nodeKey',
'type': 'bytes16'
},
{
'name': 'amount',
'type': 'uint256',
},
{
'name': 'config',
'type': 'string',
},
]
},
'primaryType': 'EIP712NodePair',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chainId,
'verifyingContract': verifyingContract,
'member': member,
'paired': paired,
'nodeId': nodeId,
'domainId': domainId,
'nodeType': nodeType,
'nodeKey': nodeKey,
'amount': amount,
'config': config or '',
}
}
return data
def sign_eip712_node_pair(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes, paired: int,
nodeId: bytes, domainId: bytes, nodeType: int, nodeKey: bytes,
amount: int, config: Optional[str]) -> bytes:
"""
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
:type eth_privkey: bytes
:return: The signature according to EIP712 (32+32+1 raw bytes).
:rtype: bytes
"""
assert is_eth_privkey(eth_privkey)
data = _create_eip712_node_pair(chainId, verifyingContract, member, paired, nodeId, domainId, nodeType,
nodeKey, amount, config)
return sign(eth_privkey, data)
def recover_eip712_node_pair(chainId: int, verifyingContract: bytes, member: bytes, paired: int,
nodeId: bytes, domainId: bytes, nodeType: int, nodeKey: bytes,
amount: int, config: str, signature: bytes) -> bytes:
"""
Recover the signer address the given EIP712 signature was signed with.
:return: The (computed) signer address the signature was signed with.
:rtype: bytes
"""
assert is_signature(signature)
data = _create_eip712_node_pair(chainId, verifyingContract, member, paired, nodeId, domainId, nodeType,
nodeKey, amount, config)
return recover(data, signature)

View File

@@ -0,0 +1,471 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
from binascii import b2a_hex
from typing import Optional, Dict, Any, List
import web3
from web3.contract import Contract
from ens import ENS
from twisted.internet.defer import Deferred, inlineCallbacks
from twisted.internet.threads import deferToThread
from autobahn.wamp.interfaces import ICryptosignKey, IEthereumKey
from autobahn.wamp.message import identify_realm_name_category
from autobahn.xbr import make_w3, EIP712AuthorityCertificate
class Seeder(object):
"""
"""
__slots__ = (
'_frealm',
'_operator',
'_label',
'_country',
'_legal',
'_endpoint',
'_bandwidth_requested',
'_bandwidth_offered',
)
def __init__(self,
frealm: 'FederatedRealm',
operator: Optional[str] = None,
label: Optional[str] = None,
country: Optional[str] = None,
legal: Optional[str] = None,
endpoint: Optional[str] = None,
bandwidth_requested: Optional[int] = None,
bandwidth_offered: Optional[int] = None,
):
"""
:param frealm:
:param operator:
:param label:
:param country:
:param legal:
:param endpoint:
:param bandwidth_requested:
:param bandwidth_offered:
"""
self._frealm: FederatedRealm = frealm
self._operator: Optional[str] = operator
self._label: Optional[str] = label
self._country: Optional[str] = country
self._legal: Optional[str] = legal
self._endpoint: Optional[str] = endpoint
self._bandwidth_requested: Optional[str] = bandwidth_requested
self._bandwidth_offered: Optional[str] = bandwidth_offered
@staticmethod
def _create_eip712_connect(chain_id: int,
verifying_contract: bytes,
channel_binding: str,
channel_id: bytes,
block_no: int,
challenge: bytes,
pubkey: bytes,
realm: bytes,
delegate: bytes,
seeder: bytes,
bandwidth: int):
channel_binding = channel_binding or ''
channel_id = channel_id or b''
assert chain_id
assert verifying_contract
assert channel_binding is not None
assert channel_id is not None
assert block_no
assert challenge
assert pubkey
assert realm
assert delegate
assert bandwidth
data = {
'types': {
'EIP712Domain': [
{
'name': 'name',
'type': 'string'
},
{
'name': 'version',
'type': 'string'
},
],
'EIP712SeederConnect': [
{
'name': 'chainId',
'type': 'uint256'
},
{
'name': 'verifyingContract',
'type': 'address'
},
{
'name': 'channel_binding',
'type': 'string'
},
{
'name': 'channel_id',
'type': 'bytes32'
},
{
'name': 'block_no',
'type': 'uint256'
},
{
'name': 'challenge',
'type': 'bytes32'
},
{
'name': 'pubkey',
'type': 'bytes32'
},
{
'name': 'realm',
'type': 'address'
},
{
'name': 'delegate',
'type': 'address'
},
{
'name': 'seeder',
'type': 'address'
},
{
'name': 'bandwidth',
'type': 'uint32'
},
]
},
'primaryType': 'EIP712SeederConnect',
'domain': {
'name': 'XBR',
'version': '1',
},
'message': {
'chainId': chain_id,
'verifyingContract': verifying_contract,
'channel_binding': channel_binding,
'channel_id': channel_id,
'block_no': block_no,
'challenge': challenge,
'pubkey': pubkey,
'realm': realm,
'delegate': delegate,
'seeder': seeder,
'bandwidth': bandwidth,
}
}
return data
@inlineCallbacks
def create_authextra(self,
client_key: ICryptosignKey,
delegate_key: IEthereumKey,
bandwidth_requested: int,
channel_id: Optional[bytes] = None,
channel_binding: Optional[str] = None) -> Deferred:
"""
:param client_key:
:param delegate_key:
:param bandwidth_requested:
:param channel_id:
:param channel_binding:
:return:
"""
chain_id = 1
# FIXME
verifying_contract = b'\x01' * 20
block_no = 1
challenge = os.urandom(32)
eip712_data = Seeder._create_eip712_connect(chain_id=chain_id,
verifying_contract=verifying_contract,
channel_binding=channel_binding,
channel_id=channel_id,
block_no=block_no,
challenge=challenge,
pubkey=client_key.public_key(binary=True),
# FIXME
# realm=self._frealm.address(binary=True),
realm=b'\x02' * 20,
delegate=delegate_key.address(binary=False),
# FIXME
# seeder=self._operator,
seeder=b'\x03' * 20,
bandwidth=bandwidth_requested)
signature = yield delegate_key.sign_typed_data(eip712_data)
authextra = {
# string
'pubkey': client_key.public_key(binary=False),
# string
'challenge': challenge,
# string
'channel_binding': channel_binding,
# string
'channel_id': channel_id,
# address
# FIXME
'realm': '7f' * 20,
# int
'chain_id': chain_id,
# int
'block_no': block_no,
# address
'delegate': delegate_key.address(binary=False),
# address
'seeder': self._operator,
# int: requested bandwidth in kbps
'bandwidth': bandwidth_requested,
# string: Eth signature by delegate_key over EIP712 typed data as above
'signature': b2a_hex(signature).decode(),
}
return authextra
@property
def frealm(self) -> 'FederatedRealm':
"""
:return:
"""
return self._frealm
@property
def operator(self) -> Optional[str]:
"""
Operator address, e.g. ``"[AWS-SECRET-REMOVED]9c"``.
:return: The Ethereum address of the endpoint operator.
"""
return self._operator
@property
def label(self) -> Optional[str]:
"""
Operator endpoint label.
:return: A human readable label for the operator or specific operator endpoint.
"""
return self._label
@property
def country(self) -> Optional[str]:
"""
Operator country (ISO 3166-1 alpha-2), e.g. ``"US"``.
:return: ISO 3166-1 alpha-2 country code.
"""
return self._country
@property
def legal(self) -> Optional[str]:
"""
:return:
"""
return self._legal
@property
def endpoint(self) -> Optional[str]:
"""
Public WAMP endpoint of seeder. Secure WebSocket URL resolving to a public IPv4
or IPv6 listening url accepting incoming WAMP-WebSocket connections,
e.g. ``wss://service1.example.com/ws``.
:return: The endpoint URL.
"""
return self._endpoint
@property
def bandwidth_requested(self) -> Optional[int]:
"""
:return:
"""
return self._bandwidth_requested
@property
def bandwidth_offered(self) -> Optional[int]:
"""
:return:
"""
return self._bandwidth_offered
class FederatedRealm(object):
"""
A federated realm is a WAMP application realm with a trust anchor rooted in Ethereum, and
which can be shared between multiple parties.
A federated realm is globally identified on an Ethereum chain (e.g. on Mainnet or Rinkeby)
by an Ethereum address associated to a federated realm owner by an on-chain record stored
in the WAMP Network contract. The federated realm address thus only needs to exist as an
identifier of the federated realm-owner record.
"""
__slots__ = (
'_name_or_address',
'_gateway_config',
'_status',
'_name_category',
'_w3',
'_ens',
'_address',
'_contract',
'_seeders',
'_root_ca',
'_catalog',
'_meta',
)
# FIXME
CONTRACT_ADDRESS = web3.Web3.toChecksumAddress('[AWS-SECRET-REMOVED]C2')
CONTRACT_ABI: str = ''
def __init__(self, name_or_address: str, gateway_config: Optional[Dict[str, Any]] = None):
"""
Instantiate a federated realm from a federated realm ENS name (which is resolved to an Ethereum
address) or Ethereum address.
:param name_or_address: Ethereum ENS name or address.
:param gateway_config: If provided, use this Ethereum gateway. If not provided,
connect via Infura to Ethereum Mainnet, which requires an environment variable
``WEB3_INFURA_PROJECT_ID`` with your Infura project ID.
"""
self._name_or_address = name_or_address
self._gateway_config = gateway_config
# status, will change to 'RUNNING' after initialize() has completed
self._status = 'STOPPED'
self._name_category: Optional[str] = identify_realm_name_category(self._name_or_address)
if self._name_category not in ['eth', 'ens', 'reverse_ens']:
raise ValueError('name_or_address "{}" not an Ethereum address or ENS name'.format(self._name_or_address))
# will be filled once initialize()'ed
self._w3 = None
self._ens = None
# address identifying the federated realm
self._address: Optional[str] = None
# will be initialized with a FederatedRealm contract instance
self._contract: Optional[Contract] = None
# cache of federated realm seeders, filled once in status running
self._seeders: List[Seeder] = []
self._root_ca = None
@property
def status(self) -> str:
return self._status
@property
def name_or_address(self) -> str:
return self._name_or_address
@property
def gateway_config(self) -> Optional[Dict[str, Any]]:
return self._gateway_config
@property
def name_category(self) -> Optional[str]:
return self._name_category
@property
def address(self):
return self._address
def root_ca(self) -> EIP712AuthorityCertificate:
assert self._status == 'RUNNING'
return self._root_ca
@property
def seeders(self) -> List[Seeder]:
return self._seeders
def initialize(self):
"""
:return:
"""
if self._status != 'STOPPED':
raise RuntimeError('cannot start in status "{}"'.format(self._status))
d = deferToThread(self._initialize_background)
return d
def _initialize_background(self):
self._status = 'STARTING'
if self._gateway_config:
self._w3 = make_w3(self._gateway_config)
else:
raise RuntimeError('cannot auto-configure ethereum connection (was removed from web3.py in v6)')
# https://github.com/ethereum/web3.py/issues/1416
# https://github.com/ethereum/web3.py/pull/2706
# from web3.auto.infura import w3
# self._w3 = w3
self._ens = ENS.from_web3(self._w3)
if self._name_category in ['ens', 'reverse_ens']:
if self._name_category == 'reverse_ens':
name = ''.join(reversed(self._name_or_address.split('.')))
else:
name = self._name_or_address
self._address = self._ens.address(name)
elif self._name_category == 'eth':
self._address = self._w3.toChecksumAddress(self._name_or_address)
else:
assert False, 'should not arrive here'
# https://web3py.readthedocs.io/en/stable/contracts.html#web3.contract.Contract
# https://web3py.readthedocs.io/en/stable/web3.eth.html#web3.eth.Eth.contract
# self._contract = self._w3.eth.contract(address=self.CONTRACT_ADDRESS, abi=self.CONTRACT_ABI)
# FIXME: get IPFS hash, download file, unzip seeders
self._status = 'RUNNING'

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,188 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import abc
class IMarketMaker(abc.ABC):
"""
XBR Market Maker interface.
"""
@abc.abstractmethod
def status(self, details):
"""
:param details:
:return:
"""
@abc.abstractmethod
def offer(self, key_id, price, details):
"""
:param key_id:
:param price:
:param details:
:return:
"""
@abc.abstractmethod
def revoke(self, key_id, details):
"""
:param key_id:
:param details:
:return:
"""
@abc.abstractmethod
def quote(self, key_id, details):
"""
:param key_id:
:param details:
:return:
"""
@abc.abstractmethod
def buy(self, channel_id, channel_seq, buyer_pubkey, datakey_id, amount, balance, signature, details):
"""
:param channel_id:
:param channel_seq:
:param buyer_pubkey:
:param datakey_id:
:param amount:
:param balance:
:param signature:
:param details:
:return:
"""
@abc.abstractmethod
def get_payment_channels(self, address, details):
"""
:param address:
:param details:
:return:
"""
@abc.abstractmethod
def get_payment_channel(self, channel_id, details):
"""
:param channel_id:
:param details:
:return:
"""
class IProvider(abc.ABC):
"""
XBR Provider interface.
"""
@abc.abstractmethod
def sell(self, key_id, buyer_pubkey, amount_paid, post_balance, signature, details):
"""
:param key_id:
:param buyer_pubkey:
:param amount_paid:
:param post_balance:
:param signature:
:param details:
:return:
"""
class IConsumer(abc.ABC):
"""
XBR Consumer interface.
"""
class ISeller(abc.ABC):
"""
XBR Seller interface.
"""
@abc.abstractmethod
async def start(self, session):
"""
:param session:
:return:
"""
@abc.abstractmethod
async def wrap(self, uri, payload):
"""
:param uri:
:param payload:
:return:
"""
class IBuyer(abc.ABC):
"""
XBR Buyer interface.
"""
@abc.abstractmethod
async def start(self, session):
"""
Start buying keys over the provided session.
:param session: WAMP session that allows to talk to the XBR Market Maker.
"""
@abc.abstractmethod
async def unwrap(self, key_id, enc_ser, ciphertext):
"""
Decrypt and deserialize received XBR payload.
:param key_id: The ID of the datakey the payload is encrypted with.
:type key_id: bytes
:param enc_ser: The serializer that was used for serializing the payload. One of ``cbor``, ``json``, ``msgpack``, ``ubjson``.
:type enc_ser: str
:param ciphertext: The encrypted payload to unwrap.
:type ciphertext: bytes
:returns: The unwrapped application payload.
:rtype: object
"""
class IDelegate(ISeller, IBuyer):
"""
XBR Delegate interface.
"""

View File

@@ -0,0 +1,167 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) 2018 Luis Teixeira
# - copied & modified from https://github.com/vergl4s/ethereum-mnemonic-utils
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import hashlib
import hmac
import struct
from base58 import b58encode_check
from ecdsa.curves import SECP256k1
BIP39_PBKDF2_ROUNDS = 2048
BIP39_SALT_MODIFIER = "mnemonic"
BIP32_PRIVDEV = 0x80000000
BIP32_CURVE = SECP256k1
BIP32_SEED_MODIFIER = b'Bitcoin seed'
# https://github.com/ethereum/EIPs/issues/84#issuecomment-528213145
LEDGER_ETH_DERIVATION_PATH = "m/44'/60'/0'/0"
__all__ = [
'mnemonic_to_bip39seed',
'mnemonic_to_private_key',
]
def mnemonic_to_bip39seed(mnemonic, passphrase):
""" BIP39 seed from a mnemonic key.
Logic adapted from https://github.com/trezor/python-mnemonic. """
mnemonic = bytes(mnemonic, 'utf8')
salt = bytes(BIP39_SALT_MODIFIER + passphrase, 'utf8')
return hashlib.pbkdf2_hmac('sha512', mnemonic, salt, BIP39_PBKDF2_ROUNDS)
def bip39seed_to_bip32masternode(seed):
""" BIP32 master node derivation from a bip39 seed.
Logic adapted from https://github.com/satoshilabs/slips/blob/master/slip-0010/testvectors.py. """
h = hmac.new(BIP32_SEED_MODIFIER, seed, hashlib.sha512).digest()
key, chain_code = h[:32], h[32:]
return key, chain_code
def derive_public_key(private_key):
""" Public key from a private key.
Logic adapted from https://github.com/satoshilabs/slips/blob/master/slip-0010/testvectors.py. """
Q = int.from_bytes(private_key, byteorder='big') * BIP32_CURVE.generator
xstr = int(Q.x()).to_bytes(32, byteorder='big')
parity = Q.y() & 1
return int(2 + parity).to_bytes(1, byteorder='big') + xstr
def derive_bip32childkey(parent_key, parent_chain_code, i):
""" Derives a child key from an existing key, i is current derivation parameter.
Logic adapted from https://github.com/satoshilabs/slips/blob/master/slip-0010/testvectors.py. """
assert len(parent_key) == 32
assert len(parent_chain_code) == 32
k = parent_chain_code
if (i & BIP32_PRIVDEV) != 0:
key = b'\x00' + parent_key
else:
key = derive_public_key(parent_key)
d = key + struct.pack('>L', i)
while True:
h = hmac.new(k, d, hashlib.sha512).digest()
key, chain_code = h[:32], h[32:]
a = int.from_bytes(key, byteorder='big')
b = int.from_bytes(parent_key, byteorder='big')
key = (a + b) % BIP32_CURVE.order
if a < BIP32_CURVE.order and key != 0:
key = int(key).to_bytes(32, byteorder='big')
break
d = b'\x01' + h[32:] + struct.pack('>L', i)
return key, chain_code
def fingerprint(public_key):
""" BIP32 fingerprint formula, used to get b58 serialized key. """
return hashlib.new('ripemd160', hashlib.sha256(public_key).digest()).digest()[:4]
def b58xprv(parent_fingerprint, private_key, chain, depth, childnr):
""" Private key b58 serialization format. """
raw = (b'\x04\x88\xad\xe4' + bytes(chr(depth), 'utf-8') + parent_fingerprint + int(childnr).to_bytes(
4, byteorder='big') + chain + b'\x00' + private_key)
return b58encode_check(raw)
def b58xpub(parent_fingerprint, public_key, chain, depth, childnr):
""" Public key b58 serialization format. """
raw = (b'\x04\x88\xb2\x1e' + bytes(chr(depth), 'utf-8') + parent_fingerprint + int(childnr).to_bytes(
4, byteorder='big') + chain + public_key)
return b58encode_check(raw)
def parse_derivation_path(str_derivation_path):
""" Parses a derivation path such as "m/44'/60/0'/0" and returns
list of integers for each element in path. """
path = []
if str_derivation_path[0:2] != 'm/':
raise ValueError("Can't recognize derivation path. It should look like \"m/44'/60/0'/0\".")
for i in str_derivation_path.lstrip('m/').split('/'):
if "'" in i:
path.append(BIP32_PRIVDEV + int(i[:-1]))
else:
path.append(int(i))
return path
def mnemonic_to_private_key(mnemonic, str_derivation_path=LEDGER_ETH_DERIVATION_PATH, passphrase=""):
""" Performs all convertions to get a private key from a mnemonic sentence, including:
BIP39 mnemonic to seed
BIP32 seed to master key
BIP32 child derivation of a path provided
Parameters:
mnemonic -- seed wordlist, usually with 24 words, that is used for ledger wallet backup
str_derivation_path -- string that directs BIP32 key derivation, defaults to path
used by ledger ETH wallet
"""
derivation_path = parse_derivation_path(str_derivation_path)
bip39seed = mnemonic_to_bip39seed(mnemonic, passphrase)
master_private_key, master_chain_code = bip39seed_to_bip32masternode(bip39seed)
private_key, chain_code = master_private_key, master_chain_code
for i in derivation_path:
private_key, chain_code = derive_bip32childkey(private_key, chain_code, i)
return private_key

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,567 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import binascii
import os
import configparser
from collections.abc import MutableMapping
from typing import Optional, Union, Dict, Any, List, Iterator
from threading import Lock
import txaio
import nacl
from eth_account.account import Account
from eth_account.signers.local import LocalAccount
from py_eth_sig_utils.eip712 import encode_typed_data
from py_eth_sig_utils.utils import ecsign, ecrecover_to_pub, checksum_encode, sha3
from py_eth_sig_utils.signing import v_r_s_to_signature, signature_to_v_r_s
from autobahn.wamp.interfaces import ISecurityModule, IEthereumKey
from autobahn.xbr._mnemonic import mnemonic_to_private_key
from autobahn.util import parse_keyfile
from autobahn.wamp.cryptosign import CryptosignKey
__all__ = ('EthereumKey', 'SecurityModuleMemory', )
class EthereumKey(object):
"""
Base class to implement :class:`autobahn.wamp.interfaces.IEthereumKey`.
"""
def __init__(self, key_or_address: Union[LocalAccount, str, bytes], can_sign: bool,
security_module: Optional[ISecurityModule] = None,
key_no: Optional[int] = None) -> None:
if can_sign:
# https://eth-account.readthedocs.io/en/latest/eth_account.html#eth_account.account.Account
assert type(key_or_address) == LocalAccount
self._key = key_or_address
self._address = key_or_address.address
else:
assert type(key_or_address) in (str, bytes)
self._key = None
self._address = key_or_address
self._can_sign = can_sign
self._security_module = security_module
self._key_no = key_no
@property
def security_module(self) -> Optional['ISecurityModule']:
"""
Implements :meth:`autobahn.wamp.interfaces.IKey.security_module`.
"""
return self._security_module
@property
def key_no(self) -> Optional[int]:
"""
Implements :meth:`autobahn.wamp.interfaces.IKey.key_no`.
"""
return self._key_no
@property
def key_type(self) -> str:
"""
Implements :meth:`autobahn.wamp.interfaces.IKey.key_type`.
"""
return 'ethereum'
def public_key(self, binary: bool = False) -> Union[str, bytes]:
"""
Implements :meth:`autobahn.wamp.interfaces.IKey.public_key`.
"""
raise NotImplementedError()
@property
def can_sign(self) -> bool:
"""
Implements :meth:`autobahn.wamp.interfaces.IKey.can_sign`.
"""
return self._can_sign
def address(self, binary: bool = False) -> Union[str, bytes]:
"""
Implements :meth:`autobahn.wamp.interfaces.IEthereumKey.address`.
"""
if binary:
return binascii.a2b_hex(self._address[2:])
else:
return self._address
def sign(self, data: bytes) -> bytes:
"""
Implements :meth:`autobahn.wamp.interfaces.IKey.sign`.
"""
# FIXME: implement signing of raw data
raise NotImplementedError()
def recover(self, data: bytes, signature: bytes) -> bytes:
"""
Implements :meth:`autobahn.wamp.interfaces.IKey.recover`.
"""
# FIXME: implement signing address recovery from signature of raw data
raise NotImplementedError()
def sign_typed_data(self, data: Dict[str, Any], binary=True) -> bytes:
"""
Implements :meth:`autobahn.wamp.interfaces.IEthereumKey.sign_typed_data`.
"""
if self._security_module:
assert self._security_module.is_open and not self._security_module.is_locked, 'security module must be open and unlocked'
try:
# encode typed data dict and return message hash
msg_hash = encode_typed_data(data)
# ECDSA signatures in Ethereum consist of three parameters: v, r and s.
# The signature is always 65-bytes in length.
# r = first 32 bytes of signature
# s = second 32 bytes of signature
# v = final 1 byte of signature
signature_vrs = ecsign(msg_hash, self._key.key)
# concatenate signature components into byte string
signature = v_r_s_to_signature(*signature_vrs)
except Exception as e:
return txaio.create_future_error(e)
else:
if binary:
return txaio.create_future_success(signature)
else:
return txaio.create_future_success(binascii.b2a_hex(signature).decode())
def verify_typed_data(self, data: Dict[str, Any], signature: bytes) -> bool:
"""
Implements :meth:`autobahn.wamp.interfaces.IEthereumKey.verify_typed_data`.
"""
if self._security_module:
assert self._security_module.is_open and not self._security_module.is_locked, 'security module must be open and unlocked'
try:
msg_hash = encode_typed_data(data)
signature_vrs = signature_to_v_r_s(signature)
public_key = ecrecover_to_pub(msg_hash, *signature_vrs)
address_bytes = sha3(public_key)[-20:]
address = checksum_encode(address_bytes)
except Exception as e:
return txaio.create_future_error(e)
else:
return txaio.create_future_success(address == self._address)
@classmethod
def from_address(cls, address: Union[str, bytes]) -> 'EthereumKey':
"""
Create a public key from an address, which can be used to verify signatures.
:param address: The Ethereum address (20 octets).
:return: New instance of :class:`EthereumKey`
"""
return EthereumKey(key_or_address=address, can_sign=False)
@classmethod
def from_bytes(cls, key: bytes) -> 'EthereumKey':
"""
Create a private key from seed bytes, which can be used to sign and create signatures.
:param key: The Ethereum private key seed (32 octets).
:return: New instance of :class:`EthereumKey`
"""
if type(key) != bytes:
raise ValueError("invalid seed type {} (expected binary)".format(type(key)))
if len(key) != 32:
raise ValueError("invalid seed length {} (expected 32)".format(len(key)))
account: LocalAccount = Account.from_key(key)
return EthereumKey(key_or_address=account, can_sign=True)
@classmethod
def from_seedphrase(cls, seedphrase: str, index: int = 0) -> 'EthereumKey':
"""
Create a private key from the given BIP-39 mnemonic seed phrase and index,
which can be used to sign and create signatures.
:param seedphrase: The BIP-39 seedphrase ("Mnemonic") from which to derive the account.
:param index: The account index in account hierarchy defined by the seedphrase.
:return: New instance of :class:`EthereumKey`
"""
# Base HD Path: m/44'/60'/0'/0/{account_index}
derivation_path = "m/44'/60'/0'/0/{}".format(index)
key = mnemonic_to_private_key(seedphrase, str_derivation_path=derivation_path)
assert type(key) == bytes
assert len(key) == 32
account: LocalAccount = Account.from_key(key)
return EthereumKey(key_or_address=account, can_sign=True)
@classmethod
def from_keyfile(cls, keyfile: str) -> 'EthereumKey':
"""
Create a public or private key from reading the given public or private key file.
Here is an example key file that includes an Ethereum private key ``private-key-eth``, which
is loaded in this function, and other fields, which are ignored by this function:
.. code-block::
This is a comment (all lines until the first empty line are comments indeed).
creator: oberstet@intel-nuci7
created-at: 2022-07-05T12:29:48.832Z
user-id: oberstet@intel-nuci7
public-key-ed25519: [AWS-SECRET-REMOVED]a81e55c434462ce31f95deed
public-adr-eth: [AWS-SECRET-REMOVED]68
private-key-ed25519: [AWS-SECRET-REMOVED]bc345c35099d3322626ab666
private-key-eth: [AWS-SECRET-REMOVED]5eac9deeb80d9f506f501025
:param keyfile: Path (relative or absolute) to a public or private keys file.
:return: New instance of :class:`EthereumKey`
"""
if not os.path.exists(keyfile) or not os.path.isfile(keyfile):
raise RuntimeError('keyfile "{}" is not a file'.format(keyfile))
# now load the private or public key file - this returns a dict which should
# include (for a private key):
#
# private-key-eth: [AWS-SECRET-REMOVED]3dc4a1cfd3a489ea387c496b
#
# or (for a public key only):
#
# public-adr-eth: [AWS-SECRET-REMOVED]68
#
data = parse_keyfile(keyfile)
privkey_eth_hex = data.get('private-key-eth', None)
if privkey_eth_hex is None:
pub_adr_eth = data.get('public-adr-eth', None)
if pub_adr_eth is None:
raise RuntimeError('neither "private-key-eth" nor "public-adr-eth" found in keyfile {}'.format(keyfile))
else:
return EthereumKey.from_address(pub_adr_eth)
else:
return EthereumKey.from_bytes(binascii.a2b_hex(privkey_eth_hex))
IEthereumKey.register(EthereumKey)
class SecurityModuleMemory(MutableMapping):
"""
A transient, memory-based implementation of :class:`ISecurityModule`.
"""
def __init__(self, keys: Optional[List[Union[CryptosignKey, EthereumKey]]] = None):
self._mutex = Lock()
self._is_open = False
self._is_locked = True
self._keys: Dict[int, Union[CryptosignKey, EthereumKey]] = {}
self._counters: Dict[int, int] = {}
if keys:
for i, key in enumerate(keys):
self._keys[i] = key
def __len__(self) -> int:
"""
Implements :meth:`ISecurityModule.__len__`
"""
assert self._is_open, 'security module not open'
return len(self._keys)
def __contains__(self, key_no: int) -> bool:
assert self._is_open, 'security module not open'
return key_no in self._keys
def __iter__(self) -> Iterator[int]:
"""
Implements :meth:`ISecurityModule.__iter__`
"""
assert self._is_open, 'security module not open'
yield from self._keys
def __getitem__(self, key_no: int) -> Union[CryptosignKey, EthereumKey]:
"""
Implements :meth:`ISecurityModule.__getitem__`
"""
assert self._is_open, 'security module not open'
if key_no in self._keys:
return self._keys[key_no]
else:
raise IndexError('key_no {} not found'.format(key_no))
def __setitem__(self, key_no: int, key: Union[CryptosignKey, EthereumKey]) -> None:
assert self._is_open, 'security module not open'
assert key_no >= 0
if key_no in self._keys:
# FIXME
pass
self._keys[key_no] = key
def __delitem__(self, key_no: int) -> None:
assert self._is_open, 'security module not open'
if key_no in self._keys:
del self._keys[key_no]
else:
raise IndexError()
def open(self):
"""
Implements :meth:`ISecurityModule.open`
"""
assert not self._is_open, 'security module already open'
self._is_open = True
return txaio.create_future_success(None)
def close(self):
"""
Implements :meth:`ISecurityModule.close`
"""
assert self._is_open, 'security module not open'
self._is_open = False
self._is_locked = True
return txaio.create_future_success(None)
@property
def is_open(self) -> bool:
"""
Implements :meth:`ISecurityModule.is_open`
"""
return self._is_open
@property
def can_lock(self) -> bool:
"""
Implements :meth:`ISecurityModule.can_lock`
"""
return True
@property
def is_locked(self) -> bool:
"""
Implements :meth:`ISecurityModule.is_locked`
"""
return self._is_locked
def lock(self):
"""
Implements :meth:`ISecurityModule.lock`
"""
assert self._is_open, 'security module not open'
assert not self._is_locked
self._is_locked = True
return txaio.create_future_success(None)
def unlock(self):
"""
Implements :meth:`ISecurityModule.unlock`
"""
assert self._is_open, 'security module not open'
assert self._is_locked
self._is_locked = False
return txaio.create_future_success(None)
def create_key(self, key_type: str) -> int:
assert self._is_open, 'security module not open'
key_no = len(self._keys)
if key_type == 'cryptosign':
key = CryptosignKey(key=nacl.signing.SigningKey(os.urandom(32)),
can_sign=True,
security_module=self,
key_no=key_no)
elif key_type == 'ethereum':
key = EthereumKey(key_or_address=Account.from_key(os.urandom(32)),
can_sign=True,
security_module=self,
key_no=key_no)
else:
raise ValueError('invalid key_type "{}"'.format(key_type))
self._keys[key_no] = key
return txaio.create_future_success(key_no)
def delete_key(self, key_no: int):
assert self._is_open, 'security module not open'
if key_no in self._keys:
del self._keys[key_no]
return txaio.create_future_success(key_no)
else:
return txaio.create_future_success(None)
def get_random(self, octets: int) -> bytes:
"""
Implements :meth:`ISecurityModule.get_random`
"""
assert self._is_open, 'security module not open'
data = os.urandom(octets)
return txaio.create_future_success(data)
def get_counter(self, counter_no: int) -> int:
"""
Implements :meth:`ISecurityModule.get_counter`
"""
assert self._is_open, 'security module not open'
self._mutex.acquire()
res = self._counters.get(counter_no, 0)
self._mutex.release()
return txaio.create_future_success(res)
def increment_counter(self, counter_no: int) -> int:
"""
Implements :meth:`ISecurityModule.increment_counter`
"""
assert self._is_open, 'security module not open'
self._mutex.acquire()
if counter_no not in self._counters:
self._counters[counter_no] = 0
self._counters[counter_no] += 1
res = self._counters[counter_no]
self._mutex.release()
return txaio.create_future_success(res)
@classmethod
def from_seedphrase(cls, seedphrase: str, num_eth_keys: int = 1,
num_cs_keys: int = 1) -> 'SecurityModuleMemory':
"""
Create a new memory-backed security module with
1. ``num_eth_keys`` keys of type :class:`EthereumKey`, followed by
2. ``num_cs_keys`` keys of type :class:`CryptosignKey`
computed from a (common) BIP44 seedphrase.
:param seedphrase: BIP44 seedphrase to use.
:param num_eth_keys: Number of Ethereum keys to derive.
:param num_cs_keys: Number of Cryptosign keys to derive.
:return: New memory-backed security module instance.
"""
keys: List[Union[EthereumKey, CryptosignKey]] = []
# first, add num_eth_keys EthereumKey(s), numbering starting at 0
for i in range(num_eth_keys):
key = EthereumKey.from_seedphrase(seedphrase, i)
keys.append(key)
# second, add num_cs_keys CryptosignKey(s), numbering starting at num_eth_keys (!)
for i in range(num_cs_keys):
key = CryptosignKey.from_seedphrase(seedphrase, i + num_eth_keys)
keys.append(key)
# initialize security module from collected keys
sm = SecurityModuleMemory(keys=keys)
return sm
@classmethod
def from_config(cls, config: str, profile: str = 'default') -> 'SecurityModuleMemory':
"""
Create a new memory-backed security module with keys referred from a profile in
the given configuration file.
:param config: Path (relative or absolute) to an INI configuration file.
:param profile: Name of the profile within the given INI configuration file.
:return: New memory-backed security module instance.
"""
keys: List[Union[EthereumKey, CryptosignKey]] = []
cfg = configparser.ConfigParser()
cfg.read(config)
if not cfg.has_section(profile):
raise RuntimeError('profile "{}" not found in configuration file "{}"'.format(profile, config))
if not cfg.has_option(profile, 'privkey'):
raise RuntimeError('missing option "privkey" in profile "{}" of configuration file "{}"'.format(profile, config))
privkey = os.path.join(os.path.dirname(config), cfg.get(profile, 'privkey'))
if not os.path.exists(privkey) or not os.path.isfile(privkey):
raise RuntimeError('privkey "{}" is not a file in profile "{}" of configuration file "{}"'.format(privkey, profile, config))
# now load the private key file - this returns a dict which should include:
# private-key-eth: [AWS-SECRET-REMOVED]3dc4a1cfd3a489ea387c496b
# private-key-ed25519: [AWS-SECRET-REMOVED]b314583d0c8d8a4942f9be40
data = parse_keyfile(privkey)
# first, add Ethereum key
privkey_eth_hex = data.get('private-key-eth', None)
keys.append(EthereumKey.from_bytes(binascii.a2b_hex(privkey_eth_hex)))
# second, add Cryptosign key
privkey_ed25519_hex = data.get('private-key-ed25519', None)
keys.append(CryptosignKey.from_bytes(binascii.a2b_hex(privkey_ed25519_hex)))
# initialize security module from collected keys
sm = SecurityModuleMemory(keys=keys)
return sm
@classmethod
def from_keyfile(cls, keyfile: str) -> 'SecurityModuleMemory':
"""
Create a new memory-backed security module with keys referred from a profile in
the given configuration file.
:param keyfile: Path (relative or absolute) to a private keys file.
:return: New memory-backed security module instance.
"""
keys: List[Union[EthereumKey, CryptosignKey]] = []
if not os.path.exists(keyfile) or not os.path.isfile(keyfile):
raise RuntimeError('keyfile "{}" is not a file'.format(keyfile))
# now load the private key file - this returns a dict which should include:
# private-key-eth: [AWS-SECRET-REMOVED]3dc4a1cfd3a489ea387c496b
# private-key-ed25519: [AWS-SECRET-REMOVED]b314583d0c8d8a4942f9be40
data = parse_keyfile(keyfile)
# first, add Ethereum key
privkey_eth_hex = data.get('private-key-eth', None)
if privkey_eth_hex is None:
raise RuntimeError('"private-key-eth" not found in keyfile {}'.format(keyfile))
keys.append(EthereumKey.from_bytes(binascii.a2b_hex(privkey_eth_hex)))
# second, add Cryptosign key
privkey_ed25519_hex = data.get('private-key-ed25519', None)
if privkey_ed25519_hex is None:
raise RuntimeError('"private-key-ed25519" not found in keyfile {}'.format(keyfile))
keys.append(CryptosignKey.from_bytes(binascii.a2b_hex(privkey_ed25519_hex)))
# initialize security module from collected keys
sm = SecurityModuleMemory(keys=keys)
return sm
ISecurityModule.register(SecurityModuleMemory)

View File

@@ -0,0 +1,726 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import asyncio
import binascii
import os
import uuid
from autobahn.wamp.types import RegisterOptions, CallDetails
from autobahn.wamp.exception import ApplicationError, TransportLost
from autobahn.wamp.protocol import ApplicationSession
from ._util import unpack_uint256, pack_uint256
from txaio import time_ns
import cbor2
import eth_keys
import nacl.secret
import nacl.utils
import nacl.public
import txaio
from ..util import hl, hlval
from ._eip712_channel_close import sign_eip712_channel_close, recover_eip712_channel_close
class KeySeries(object):
"""
Data encryption key series with automatic (time-based) key rotation
and key offering (to the XBR market maker).
"""
def __init__(self, api_id, price, interval=None, count=None, on_rotate=None):
"""
:param api_id: ID of the API for which to generate keys.
:type api_id: bytes
:param price: Price per key in key series.
:type price: int
:param interval: Interval in seconds after which to auto-rotate key.
:type interval: int
:param count: Number of encryption operations after which to auto-rotate key.
:type count: int
:param on_rotate: Optional user callback fired after key was rotated.
:type on_rotate: callable
"""
assert type(api_id) == bytes and len(api_id) == 16
assert type(price) == int and price >= 0
assert interval is None or (type(interval) == int and interval > 0)
assert count is None or (type(count) == int and count > 0)
assert (interval is None and count is not None) or (interval is not None and count is None)
assert on_rotate is None or callable(on_rotate)
self._api_id = api_id
self._price = price
self._interval = interval
self._count = count
self._count_current = 0
self._on_rotate = on_rotate
self._id = None
self._key = None
self._box = None
self._archive = {}
@property
def key_id(self):
"""
Get current XBR data encryption key ID (of the keys being rotated
in a series).
:return: Current key ID in key series (16 bytes).
:rtype: bytes
"""
return self._id
async def encrypt(self, payload):
"""
Encrypt data with the current XBR data encryption key.
:param payload: Application payload to encrypt.
:type payload: object
:return: The ciphertext for the encrypted application payload.
:rtype: bytes
"""
data = cbor2.dumps(payload)
if self._count is not None:
self._count_current += 1
if self._count_current >= self._count:
await self._rotate()
self._count_current = 0
ciphertext = self._box.encrypt(data)
return self._id, 'cbor', ciphertext
def encrypt_key(self, key_id, buyer_pubkey):
"""
Encrypt a (previously used) XBR data encryption key with a buyer public key.
:param key_id: ID of the data encryption key to encrypt.
:type key_id: bytes
:param buyer_pubkey: Buyer WAMP public key (Ed25519) to asymmetrically encrypt
the data encryption key (selected by ``key_id``) against.
:type buyer_pubkey: bytes
:return: The ciphertext for the encrypted data encryption key.
:rtype: bytes
"""
assert type(key_id) == bytes and len(key_id) == 16
assert type(buyer_pubkey) == bytes and len(buyer_pubkey) == 32
key, _ = self._archive[key_id]
sendkey_box = nacl.public.SealedBox(nacl.public.PublicKey(buyer_pubkey,
encoder=nacl.encoding.RawEncoder))
encrypted_key = sendkey_box.encrypt(key, encoder=nacl.encoding.RawEncoder)
return encrypted_key
def start(self):
raise NotImplementedError()
def stop(self):
raise NotImplementedError()
async def _rotate(self):
# generate new ID for next key in key series
self._id = os.urandom(16)
# generate next data encryption key in key series
self._key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)
# create secretbox from new key
self._box = nacl.secret.SecretBox(self._key)
# add key to archive
self._archive[self._id] = (self._key, self._box)
self.log.debug(
'{tx_type} key "{key_id}" rotated [api_id="{api_id}"]',
tx_type=hl('XBR ROTATE', color='magenta'),
key_id=hl(uuid.UUID(bytes=self._id)),
api_id=hl(uuid.UUID(bytes=self._api_id)))
# maybe fire user callback
if self._on_rotate:
await self._on_rotate(self)
class PayingChannel(object):
def __init__(self, adr, seq, balance):
assert type(adr) == bytes and len(adr) == 16
assert type(seq) == int and seq >= 0
assert type(balance) == int and balance >= 0
self._adr = adr
self._seq = seq
self._balance = balance
class SimpleSeller(object):
log = None
KeySeries = None
STATE_NONE = 0
STATE_STARTING = 1
STATE_STARTED = 2
STATE_STOPPING = 3
STATE_STOPPED = 4
def __init__(self, market_maker_adr, seller_key, provider_id=None):
"""
:param market_maker_adr: Market maker public Ethereum address (20 bytes).
:type market_maker_adr: bytes
:param seller_key: Seller (delegate) private Ethereum key (32 bytes).
:type seller_key: bytes
:param provider_id: Optional explicit data provider ID. When not given, the seller delegate
public WAMP key (Ed25519 in Hex) is used as the provider ID. This must be a valid WAMP URI part.
:type provider_id: string
"""
assert type(market_maker_adr) == bytes and len(market_maker_adr) == 20, 'market_maker_adr must be bytes[20], but got "{}"'.format(market_maker_adr)
assert type(seller_key) == bytes and len(seller_key) == 32, 'seller delegate must be bytes[32], but got "{}"'.format(seller_key)
assert provider_id is None or type(provider_id) == str, 'provider_id must be None or string, but got "{}"'.format(provider_id)
self.log = txaio.make_logger()
# current seller state
self._state = SimpleSeller.STATE_NONE
# market maker address
self._market_maker_adr = market_maker_adr
self._xbrmm_config = None
# seller raw ethereum private key (32 bytes)
self._pkey_raw = seller_key
# seller ethereum private key object
self._pkey = eth_keys.keys.PrivateKey(seller_key)
# seller ethereum private account from raw private key
# FIXME
# self._acct = Account.privateKeyToAccount(self._pkey)
self._acct = None
# seller ethereum account canonical address
self._addr = self._pkey.public_key.to_canonical_address()
# seller ethereum account canonical checksummed address
# FIXME
# self._caddr = web3.Web3.toChecksumAddress(self._addr)
self._caddr = None
# seller provider ID
self._provider_id = provider_id or str(self._pkey.public_key)
self._channels = {}
# will be filled with on-chain payment channel contract, once started
self._channel = None
# channel current (off-chain) balance
self._balance = 0
# channel sequence number
self._seq = 0
self._keys = {}
self._keys_map = {}
# after start() is running, these will be set
self._session = None
self._session_regs = None
@property
def public_key(self):
"""
This seller delegate public Ethereum key.
:return: Ethereum public key of this seller delegate.
:rtype: bytes
"""
return self._pkey.public_key
def add(self, api_id, prefix, price, interval=None, count=None, categories=None):
"""
Add a new (rotating) private encryption key for encrypting data on the given API.
:param api_id: API for which to create a new series of rotating encryption keys.
:type api_id: bytes
:param price: Price in XBR token per key.
:type price: int
:param interval: Interval (in seconds) after which to auto-rotate the encryption key.
:type interval: int
:param count: Number of encryption operations after which to auto-rotate the encryption key.
:type count: int
"""
assert type(api_id) == bytes and len(api_id) == 16 and api_id not in self._keys
assert type(price) == int and price >= 0
assert interval is None or (type(interval) == int and interval > 0)
assert count is None or (type(count) == int and count > 0)
assert (interval is None and count is not None) or (interval is not None and count is None)
assert categories is None or (type(categories) == dict and (type(k) == str for k in categories.keys()) and (type(v) == str for v in categories.values())), 'invalid categories type (must be dict) or category key or value type (must both be string)'
async def on_rotate(key_series):
key_id = key_series.key_id
self._keys_map[key_id] = key_series
# FIXME: expose the knobs hard-coded in below ..
# offer the key to the market maker (retry 5x in specific error cases)
retries = 5
while retries:
try:
valid_from = time_ns() - 10 * 10 ** 9
delegate = self._addr
# FIXME: sign the supplied offer information using self._pkey
signature = os.urandom(65)
provider_id = self._provider_id
offer = await self._session.call('xbr.marketmaker.place_offer',
key_id,
api_id,
prefix,
valid_from,
delegate,
signature,
privkey=None,
price=pack_uint256(price) if price is not None else None,
categories=categories,
expires=None,
copies=None,
provider_id=provider_id)
self.log.debug(
'{tx_type} key "{key_id}" offered for {price} [api_id={api_id}, prefix="{prefix}", delegate="{delegate}"]',
tx_type=hl('XBR OFFER ', color='magenta'),
key_id=hl(uuid.UUID(bytes=key_id)),
api_id=hl(uuid.UUID(bytes=api_id)),
price=hl(str(int(price / 10 ** 18) if price is not None else 0) + ' XBR', color='magenta'),
delegate=hl(binascii.b2a_hex(delegate).decode()),
prefix=hl(prefix))
self.log.debug('offer={offer}', offer=offer)
break
except ApplicationError as e:
if e.error == 'wamp.error.no_such_procedure':
self.log.warn('xbr.marketmaker.offer: procedure unavailable!')
else:
self.log.failure()
break
except TransportLost:
self.log.warn('TransportLost while calling xbr.marketmaker.offer!')
break
except:
self.log.failure()
retries -= 1
self.log.warn('Failed to place offer for key! Retrying {retries}/5 ..', retries=retries)
await asyncio.sleep(1)
key_series = self.KeySeries(api_id, price, interval=interval, count=count, on_rotate=on_rotate)
self._keys[api_id] = key_series
self.log.debug('Created new key series {key_series}', key_series=key_series)
return key_series
async def start(self, session):
"""
Start rotating keys and placing key offers with the XBR market maker.
:param session: WAMP session over which to communicate with the XBR market maker.
:type session: :class:`autobahn.wamp.protocol.ApplicationSession`
"""
assert isinstance(session, ApplicationSession), 'session must be an ApplicationSession, was "{}"'.format(session)
assert self._state in [SimpleSeller.STATE_NONE, SimpleSeller.STATE_STOPPED], 'seller already running'
self._state = SimpleSeller.STATE_STARTING
self._session = session
self._session_regs = []
self.log.debug('Start selling from seller delegate address {address} (public key 0x{public_key}..)',
address=hl(self._caddr),
public_key=binascii.b2a_hex(self._pkey.public_key[:10]).decode())
# get the currently active (if any) paying channel for the delegate
self._channel = await session.call('xbr.marketmaker.get_active_paying_channel', self._addr)
if not self._channel:
raise Exception('no active paying channel found')
channel_oid = self._channel['channel_oid']
assert type(channel_oid) == bytes and len(channel_oid) == 16
self._channel_oid = uuid.UUID(bytes=channel_oid)
procedure = 'xbr.provider.{}.sell'.format(self._provider_id)
reg = await session.register(self.sell, procedure, options=RegisterOptions(details_arg='details'))
self._session_regs.append(reg)
self.log.debug('Registered procedure "{procedure}"', procedure=hl(reg.procedure))
procedure = 'xbr.provider.{}.close_channel'.format(self._provider_id)
reg = await session.register(self.close_channel, procedure, options=RegisterOptions(details_arg='details'))
self._session_regs.append(reg)
self.log.debug('Registered procedure "{procedure}"', procedure=hl(reg.procedure))
for key_series in self._keys.values():
await key_series.start()
self._xbrmm_config = await session.call('xbr.marketmaker.get_config')
# get the current (off-chain) balance of the paying channel
paying_balance = await session.call('xbr.marketmaker.get_paying_channel_balance', self._channel_oid.bytes)
# FIXME
if type(paying_balance['remaining']) == bytes:
paying_balance['remaining'] = unpack_uint256(paying_balance['remaining'])
if not paying_balance['remaining'] > 0:
raise Exception('no off-chain balance remaining on paying channel')
self._channels[channel_oid] = PayingChannel(channel_oid, paying_balance['seq'], paying_balance['remaining'])
self._state = SimpleSeller.STATE_STARTED
# FIXME
self._balance = paying_balance['remaining']
if type(self._balance) == bytes:
self._balance = unpack_uint256(self._balance)
self._seq = paying_balance['seq']
self.log.info('Ok, seller delegate started [active paying channel {channel_oid} with remaining balance {remaining} at sequence {seq}]',
channel_oid=hl(self._channel_oid), remaining=hlval(self._balance), seq=hlval(self._seq))
return paying_balance['remaining']
async def stop(self):
"""
Stop rotating/offering keys to the XBR market maker.
"""
assert self._state in [SimpleSeller.STATE_STARTED], 'seller not running'
self._state = SimpleSeller.STATE_STOPPING
dl = []
for key_series in self._keys.values():
d = key_series.stop()
dl.append(d)
if self._session_regs:
if self._session and self._session.is_attached():
# voluntarily unregister interface
for reg in self._session_regs:
d = reg.unregister()
dl.append(d)
self._session_regs = None
d = txaio.gather(dl)
try:
await d
except:
self.log.failure()
finally:
self._state = SimpleSeller.STATE_STOPPED
self._session = None
self.log.info('Ok, seller delegate stopped.')
async def balance(self):
"""
Return current (off-chain) balance of paying channel:
* ``amount``: The initial amount with which the paying channel was opened.
* ``remaining``: The remaining amount of XBR in the paying channel that can be earned.
* ``inflight``: The amount of XBR allocated to sell transactions that are currently processed.
:return: Current paying balance.
:rtype: dict
"""
if self._state not in [SimpleSeller.STATE_STARTED]:
raise RuntimeError('seller not running')
if not self._session or not self._session.is_attached():
raise RuntimeError('market-maker session not attached')
paying_balance = await self._session.call('xbr.marketmaker.get_paying_channel_balance', self._channel['channel_oid'])
return paying_balance
async def wrap(self, api_id, uri, payload):
"""
Encrypt and wrap application payload for a given API and destined for a specific WAMP URI.
:param api_id: API for which to encrypt and wrap the application payload for.
:type api_id: bytes
:param uri: WAMP URI the application payload is destined for (eg the procedure or topic URI).
:type uri: str
:param payload: Application payload to encrypt and wrap.
:type payload: object
:return: The encrypted and wrapped application payload: a tuple with ``(key_id, serializer, ciphertext)``.
:rtype: tuple
"""
assert type(api_id) == bytes and len(api_id) == 16 and api_id in self._keys
assert type(uri) == str
assert payload is not None
keyseries = self._keys[api_id]
key_id, serializer, ciphertext = await keyseries.encrypt(payload)
return key_id, serializer, ciphertext
def close_channel(self, market_maker_adr, channel_oid, channel_seq, channel_balance, channel_is_final,
marketmaker_signature, details=None):
"""
Called by a XBR Market Maker to close a paying channel.
"""
assert type(market_maker_adr) == bytes and len(market_maker_adr) == 20, 'market_maker_adr must be bytes[20], but was {}'.format(type(market_maker_adr))
assert type(channel_oid) == bytes and len(channel_oid) == 16, 'channel_oid must be bytes[16], but was {}'.format(type(channel_oid))
assert type(channel_seq) == int, 'channel_seq must be int, but was {}'.format(type(channel_seq))
assert type(channel_balance) == bytes and len(channel_balance) == 32, 'channel_balance must be bytes[32], but was {}'.format(type(channel_balance))
assert type(channel_is_final) == bool, 'channel_is_final must be bool, but was {}'.format(type(channel_is_final))
assert type(marketmaker_signature) == bytes and len(marketmaker_signature) == (32 + 32 + 1), 'marketmaker_signature must be bytes[65], but was {}'.format(type(marketmaker_signature))
assert details is None or isinstance(details, CallDetails), 'details must be autobahn.wamp.types.CallDetails'
# check that the delegate_adr fits what we expect for the market maker
if market_maker_adr != self._market_maker_adr:
raise ApplicationError('xbr.error.unexpected_delegate_adr',
'{}.sell() - unexpected market maker (delegate) address: expected 0x{}, but got 0x{}'.format(self.__class__.__name__, binascii.b2a_hex(self._market_maker_adr).decode(), binascii.b2a_hex(market_maker_adr).decode()))
# FIXME: must be the currently active channel .. and we need to track all of these
if channel_oid != self._channel['channel_oid']:
self._session.leave()
raise ApplicationError('xbr.error.unexpected_channel_oid',
'{}.sell() - unexpected paying channel address: expected 0x{}, but got 0x{}'.format(self.__class__.__name__, binascii.b2a_hex(self._channel['channel_oid']).decode(), binascii.b2a_hex(channel_oid).decode()))
# channel sequence number: check we have consensus on off-chain channel state with peer (which is the market maker)
if channel_seq != self._seq:
raise ApplicationError('xbr.error.unexpected_channel_seq',
'{}.sell() - unexpected channel (after tx) sequence number: expected {}, but got {}'.format(self.__class__.__name__, self._seq + 1, channel_seq))
# channel balance: check we have consensus on off-chain channel state with peer (which is the market maker)
channel_balance = unpack_uint256(channel_balance)
if channel_balance != self._balance:
raise ApplicationError('xbr.error.unexpected_channel_balance',
'{}.sell() - unexpected channel (after tx) balance: expected {}, but got {}'.format(self.__class__.__name__, self._balance, channel_balance))
# XBRSIG: check the signature (over all input data for the buying of the key)
signer_address = recover_eip712_channel_close(channel_oid, channel_seq, channel_balance, channel_is_final, marketmaker_signature)
if signer_address != market_maker_adr:
self.log.warn('{klass}.sell()::XBRSIG[4/8] - EIP712 signature invalid: signer_address={signer_address}, delegate_adr={delegate_adr}',
klass=self.__class__.__name__,
signer_address=hl(binascii.b2a_hex(signer_address).decode()),
delegate_adr=hl(binascii.b2a_hex(market_maker_adr).decode()))
raise ApplicationError('xbr.error.invalid_signature', '{}.sell()::XBRSIG[4/8] - EIP712 signature invalid or not signed by market maker'.format(self.__class__.__name__))
# XBRSIG: compute EIP712 typed data signature
seller_signature = sign_eip712_channel_close(self._pkey_raw, channel_oid, channel_seq, channel_balance, channel_is_final)
receipt = {
'delegate': self._addr,
'seq': channel_seq,
'balance': pack_uint256(channel_balance),
'is_final': channel_is_final,
'signature': seller_signature,
}
self.log.debug('{klass}.close_channel() - {tx_type} closing channel {channel_oid}, closing balance {channel_balance}, closing sequence {channel_seq} [caller={caller}, caller_authid="{caller_authid}"]',
klass=self.__class__.__name__,
tx_type=hl('XBR CLOSE ', color='magenta'),
channel_balance=hl(str(int(channel_balance / 10 ** 18)) + ' XBR', color='magenta'),
channel_seq=hl(channel_seq),
channel_oid=hl(binascii.b2a_hex(channel_oid).decode()),
caller=hl(details.caller),
caller_authid=hl(details.caller_authid))
return receipt
def sell(self, market_maker_adr, buyer_pubkey, key_id, channel_oid, channel_seq, amount, balance, signature, details=None):
"""
Called by a XBR Market Maker to buy a data encyption key. The XBR Market Maker here is
acting for (triggered by) the XBR buyer delegate.
:param market_maker_adr: The market maker Ethereum address. The technical buyer is usually the
XBR market maker (== the XBR delegate of the XBR market operator).
:type market_maker_adr: bytes of length 20
:param buyer_pubkey: The buyer delegate Ed25519 public key.
:type buyer_pubkey: bytes of length 32
:param key_id: The UUID of the data encryption key to buy.
:type key_id: bytes of length 16
:param channel_oid: The on-chain channel contract address.
:type channel_oid: bytes of length 16
:param channel_seq: Paying channel sequence off-chain transaction number.
:type channel_seq: int
:param amount: The amount paid by the XBR Buyer via the XBR Market Maker.
:type amount: bytes
:param balance: Balance remaining in the payment channel (from the market maker to the
seller) after successfully buying the key.
:type balance: bytes
:param signature: Signature over the supplied buying information, using the Ethereum
private key of the market maker (which is the delegate of the marker operator).
:type signature: bytes of length 65
:param details: Caller details. The call will come from the XBR Market Maker.
:type details: :class:`autobahn.wamp.types.CallDetails`
:return: The data encryption key, itself encrypted to the public key of the original buyer.
:rtype: bytes
"""
assert type(market_maker_adr) == bytes and len(market_maker_adr) == 20, 'delegate_adr must be bytes[20]'
assert type(buyer_pubkey) == bytes and len(buyer_pubkey) == 32, 'buyer_pubkey must be bytes[32]'
assert type(key_id) == bytes and len(key_id) == 16, 'key_id must be bytes[16]'
assert type(channel_oid) == bytes and len(channel_oid) == 16, 'channel_oid must be bytes[16]'
assert type(channel_seq) == int, 'channel_seq must be int'
assert type(amount) == bytes and len(amount) == 32, 'amount_paid must be bytes[32], but was {}'.format(type(amount))
assert type(balance) == bytes and len(amount) == 32, 'post_balance must be bytes[32], but was {}'.format(type(balance))
assert type(signature) == bytes and len(signature) == (32 + 32 + 1), 'signature must be bytes[65]'
assert details is None or isinstance(details, CallDetails), 'details must be autobahn.wamp.types.CallDetails'
amount = unpack_uint256(amount)
balance = unpack_uint256(balance)
# check that the delegate_adr fits what we expect for the market maker
if market_maker_adr != self._market_maker_adr:
raise ApplicationError('xbr.error.unexpected_marketmaker_adr',
'{}.sell() - unexpected market maker address: expected 0x{}, but got 0x{}'.format(self.__class__.__name__, binascii.b2a_hex(self._market_maker_adr).decode(), binascii.b2a_hex(market_maker_adr).decode()))
# get the key series given the key_id
if key_id not in self._keys_map:
raise ApplicationError('crossbar.error.no_such_object', '{}.sell() - no key with ID "{}"'.format(self.__class__.__name__, key_id))
key_series = self._keys_map[key_id]
# FIXME: must be the currently active channel .. and we need to track all of these
if channel_oid != self._channel['channel_oid']:
self._session.leave()
raise ApplicationError('xbr.error.unexpected_channel_oid',
'{}.sell() - unexpected paying channel address: expected 0x{}, but got 0x{}'.format(self.__class__.__name__, binascii.b2a_hex(self._channel['channel_oid']).decode(), binascii.b2a_hex(channel_oid).decode()))
# channel sequence number: check we have consensus on off-chain channel state with peer (which is the market maker)
if channel_seq != self._seq + 1:
raise ApplicationError('xbr.error.unexpected_channel_seq',
'{}.sell() - unexpected channel (after tx) sequence number: expected {}, but got {}'.format(self.__class__.__name__, self._seq + 1, channel_seq))
# channel balance: check we have consensus on off-chain channel state with peer (which is the market maker)
if balance != self._balance - amount:
raise ApplicationError('xbr.error.unexpected_channel_balance',
'{}.sell() - unexpected channel (after tx) balance: expected {}, but got {}'.format(self.__class__.__name__, self._balance - amount, balance))
# FIXME
current_block_number = 1
verifying_chain_id = self._xbrmm_config['verifying_chain_id']
verifying_contract_adr = binascii.a2b_hex(self._xbrmm_config['verifying_contract_adr'][2:])
market_oid = self._channel['market_oid']
# XBRSIG[4/8]: check the signature (over all input data for the buying of the key)
signer_address = recover_eip712_channel_close(verifying_chain_id, verifying_contract_adr, current_block_number,
market_oid, channel_oid, channel_seq, balance, False, signature)
if signer_address != market_maker_adr:
self.log.warn('{klass}.sell()::XBRSIG[4/8] - EIP712 signature invalid: signer_address={signer_address}, delegate_adr={delegate_adr}',
klass=self.__class__.__name__,
signer_address=hl(binascii.b2a_hex(signer_address).decode()),
delegate_adr=hl(binascii.b2a_hex(market_maker_adr).decode()))
raise ApplicationError('xbr.error.invalid_signature', '{}.sell()::XBRSIG[4/8] - EIP712 signature invalid or not signed by market maker'.format(self.__class__.__name__))
# now actually update our local knowledge of the channel state
# FIXME: what if code down below fails?
self._seq += 1
self._balance -= amount
# encrypt the data encryption key against the original buyer delegate Ed25519 public key
sealed_key = key_series.encrypt_key(key_id, buyer_pubkey)
assert type(sealed_key) == bytes and len(sealed_key) == 80, '{}.sell() - unexpected sealed key computed (expected bytes[80]): {}'.format(self.__class__.__name__, sealed_key)
# XBRSIG[5/8]: compute EIP712 typed data signature
seller_signature = sign_eip712_channel_close(self._pkey_raw, verifying_chain_id, verifying_contract_adr,
current_block_number, market_oid, channel_oid, self._seq,
self._balance, False)
receipt = {
# key ID that has been bought
'key_id': key_id,
# seller delegate address that sold the key
'delegate': self._addr,
# buyer delegate Ed25519 public key with which the bought key was sealed
'buyer_pubkey': buyer_pubkey,
# finally return what the consumer (buyer) was actually interested in:
# the data encryption key, sealed (public key Ed25519 encrypted) to the
# public key of the buyer delegate
'sealed_key': sealed_key,
# paying channel off-chain transaction sequence numbers
'channel_seq': self._seq,
# amount paid for the key
'amount': amount,
# paying channel amount remaining
'balance': self._balance,
# seller (delegate) signature
'signature': seller_signature,
}
self.log.info('{klass}.sell() - {tx_type} key "{key_id}" sold for {amount_earned} - balance is {balance} [caller={caller}, caller_authid="{caller_authid}", buyer_pubkey="{buyer_pubkey}"]',
klass=self.__class__.__name__,
tx_type=hl('XBR SELL ', color='magenta'),
key_id=hl(uuid.UUID(bytes=key_id)),
amount_earned=hl(str(int(amount / 10 ** 18)) + ' XBR', color='magenta'),
balance=hl(str(int(self._balance / 10 ** 18)) + ' XBR', color='magenta'),
# paying_channel=hl(binascii.b2a_hex(paying_channel).decode()),
caller=hl(details.caller),
caller_authid=hl(details.caller_authid),
buyer_pubkey=hl(binascii.b2a_hex(buyer_pubkey).decode()))
return receipt

View File

@@ -0,0 +1,244 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import re
import os
import binascii
import socket
from collections import OrderedDict
import getpass
import click
from nacl.signing import SigningKey
from nacl.encoding import HexEncoder
from eth_keys import KeyAPI
from eth_keys.backends import NativeECCBackend
from autobahn.util import utcnow, write_keyfile, parse_keyfile
from autobahn.wamp import cryptosign
if 'USER' in os***REMOVED***iron:
_DEFAULT_EMAIL_ADDRESS = '{}@{}'.format(os***REMOVED***iron['USER'], socket.getfqdn())
else:
_DEFAULT_EMAIL_ADDRESS = 'unknown'
class EmailAddress(click.ParamType):
"""
Email address validator.
"""
name = 'Email address'
def __init__(self):
click.ParamType.__init__(self)
def convert(self, value, param, ctx):
if re.match(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", value):
return value
self.fail('invalid email address "{}"'.format(value))
def _user_id(yes_to_all=False):
if yes_to_all:
return _DEFAULT_EMAIL_ADDRESS
while True:
value = click.prompt('Please enter your email address', type=EmailAddress(), default=_DEFAULT_EMAIL_ADDRESS)
if click.confirm('We will send an activation code to "{}", ok?'.format(value), default=True):
break
return value
def _creator(yes_to_all=False):
"""
for informational purposes, try to identify the creator (user@hostname)
"""
if yes_to_all:
return _DEFAULT_EMAIL_ADDRESS
else:
try:
user = getpass.getuser()
except BaseException:
user = 'unknown'
try:
hostname = socket.gethostname()
except BaseException:
hostname = 'unknown'
return '{}@{}'.format(user, hostname)
class UserKey(object):
def __init__(self, privkey, pubkey, yes_to_all=True):
self._privkey_path = privkey
self._pubkey_path = pubkey
self.key = None
self._creator = None
self._created_at = None
self.user_id = None
self._privkey = None
self._privkey_hex = None
self._pubkey = None
self._pubkey_hex = None
self._load_and_maybe_generate(self._privkey_path, self._pubkey_path, yes_to_all)
def __str__(self):
return 'UserKey(privkey="{}", pubkey="{}" [{}])'.format(self._privkey_path, self._pubkey_path,
self._pubkey_hex)
def _load_and_maybe_generate(self, privkey_path, pubkey_path, yes_to_all=False):
if os.path.exists(privkey_path):
# node private key seems to exist already .. check!
priv_tags = parse_keyfile(privkey_path, private=True)
for tag in ['creator', 'created-at', 'user-id', 'public-key-ed25519', 'private-key-ed25519']:
if tag not in priv_tags:
raise Exception("Corrupt user private key file {} - {} tag not found".format(privkey_path, tag))
creator = priv_tags['creator']
created_at = priv_tags['created-at']
user_id = priv_tags['user-id']
privkey_hex = priv_tags['private-key-ed25519']
privkey = SigningKey(privkey_hex, encoder=HexEncoder)
pubkey = privkey.verify_key
pubkey_hex = pubkey.encode(encoder=HexEncoder).decode('ascii')
if priv_tags['public-key-ed25519'] != pubkey_hex:
raise Exception(("Inconsistent user private key file {} - public-key-ed25519 doesn't"
" correspond to private-key-ed25519").format(pubkey_path))
eth_pubadr = None
eth_privkey = None
eth_privkey_seed_hex = priv_tags.get('private-key-eth', None)
if eth_privkey_seed_hex:
eth_privkey_seed = binascii.a2b_hex(eth_privkey_seed_hex)
eth_privkey = KeyAPI(NativeECCBackend).PrivateKey(eth_privkey_seed)
eth_pubadr = eth_privkey.public_key.to_checksum_address()
if 'public-adr-eth' in priv_tags:
if priv_tags['public-adr-eth'] != eth_pubadr:
raise Exception(("Inconsistent node private key file {} - public-adr-eth doesn't"
" correspond to private-key-eth").format(privkey_path))
if os.path.exists(pubkey_path):
pub_tags = parse_keyfile(pubkey_path, private=False)
for tag in ['creator', 'created-at', 'user-id', 'public-key-ed25519']:
if tag not in pub_tags:
raise Exception("Corrupt user public key file {} - {} tag not found".format(pubkey_path, tag))
if pub_tags['public-key-ed25519'] != pubkey_hex:
raise Exception(("Inconsistent user public key file {} - public-key-ed25519 doesn't"
" correspond to private-key-ed25519").format(pubkey_path))
if pub_tags.get('public-adr-eth', None) != eth_pubadr:
raise Exception(
("Inconsistent user public key file {} - public-adr-eth doesn't"
" correspond to private-key-eth in private key file {}").format(pubkey_path, privkey_path))
else:
# public key is missing! recreate it
pub_tags = OrderedDict([
('creator', priv_tags['creator']),
('created-at', priv_tags['created-at']),
('user-id', priv_tags['user-id']),
('public-key-ed25519', pubkey_hex),
('public-adr-eth', eth_pubadr),
])
msg = 'Crossbar.io user public key\n\n'
write_keyfile(pubkey_path, pub_tags, msg)
click.echo('Re-created user public key from private key: {}'.format(pubkey_path))
else:
# user private key does not yet exist: generate one
creator = _creator(yes_to_all)
created_at = utcnow()
user_id = _user_id(yes_to_all)
privkey = SigningKey.generate()
privkey_hex = privkey.encode(encoder=HexEncoder).decode('ascii')
pubkey = privkey.verify_key
pubkey_hex = pubkey.encode(encoder=HexEncoder).decode('ascii')
eth_privkey_seed = os.urandom(32)
eth_privkey_seed_hex = binascii.b2a_hex(eth_privkey_seed).decode()
eth_privkey = KeyAPI(NativeECCBackend).PrivateKey(eth_privkey_seed)
eth_pubadr = eth_privkey.public_key.to_checksum_address()
# first, write the public file
tags = OrderedDict([
('creator', creator),
('created-at', created_at),
('user-id', user_id),
('public-key-ed25519', pubkey_hex),
('public-adr-eth', eth_pubadr),
])
msg = 'Crossbar.io user public key\n\n'
write_keyfile(pubkey_path, tags, msg)
os.chmod(pubkey_path, 420)
# now, add the private key and write the private file
tags['private-key-ed25519'] = privkey_hex
tags['private-key-eth'] = eth_privkey_seed_hex
msg = 'Crossbar.io user private key - KEEP THIS SAFE!\n\n'
write_keyfile(privkey_path, tags, msg)
os.chmod(privkey_path, 384)
click.echo('New user public key generated: {}'.format(pubkey_path))
click.echo('New user private key generated ({}): {}'.format('keep this safe!', privkey_path))
# fix file permissions on node public/private key files
# note: we use decimals instead of octals as octal literals have changed between Py2/3
if os.stat(pubkey_path).st_mode & 511 != 420: # 420 (decimal) == 0644 (octal)
os.chmod(pubkey_path, 420)
click.echo('File permissions on user public key fixed!')
if os.stat(privkey_path).st_mode & 511 != 384: # 384 (decimal) == 0600 (octal)
os.chmod(privkey_path, 384)
click.echo('File permissions on user private key fixed!')
# load keys into object
self._creator = creator
self._created_at = created_at
self._privkey = privkey
self._privkey_hex = privkey_hex
self._pubkey = pubkey
self._pubkey_hex = pubkey_hex
self._eth_pubadr = eth_pubadr
self._eth_privkey_seed_hex = eth_privkey_seed_hex
self._eth_privkey = eth_privkey
self.user_id = user_id
self.key = cryptosign.CryptosignKey(privkey, can_sign=True)

View File

@@ -0,0 +1,175 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import struct
from binascii import a2b_hex, b2a_hex
from typing import Union, Dict, List
import web3
def make_w3(gateway_config=None):
"""
Create a Web3 instance configured and ready-to-use gateway to the blockchain.
:param gateway_config: Blockchain gateway configuration.
:type gateway_config: dict
:return: Configured Web3 instance.
:rtype: :class:`web3.Web3`
"""
if gateway_config is None or gateway_config['type'] == 'auto':
w3 = web3.Web3()
elif gateway_config['type'] == 'user':
request_kwargs = gateway_config.get('http_options', {})
w3 = web3.Web3(web3.Web3.HTTPProvider(gateway_config['http'], request_kwargs=request_kwargs))
elif gateway_config['type'] == 'infura':
request_kwargs = gateway_config.get('http_options', {})
project_id = gateway_config['key']
# project_secret = gateway_config['secret']
http_url = 'https://{}.infura.io/v3/{}'.format(gateway_config['network'], project_id)
w3 = web3.Web3(web3.Web3.HTTPProvider(http_url, request_kwargs=request_kwargs))
# https://web3py.readthedocs.io/en/stable/middleware.html#geth-style-proof-of-authority
if gateway_config.get('network', None) == 'rinkeby':
# This middleware is required to connect to geth --dev or the Rinkeby public network.
from web3.middleware import geth_poa_middleware
# inject the poa compatibility middleware to the innermost layer
w3.middleware_onion.inject(geth_poa_middleware, layer=0)
# FIXME
elif gateway_config['type'] == 'cloudflare':
# https://developers.cloudflare.com/web3/ethereum-gateway/reference/supported-networks/
raise NotImplementedError()
# FIXME
elif gateway_config['type'] == 'zksync':
# https://v2-docs.zksync.io/dev/testnet/important-links.html
raise NotImplementedError()
else:
raise RuntimeError('invalid blockchain gateway type "{}"'.format(gateway_config['type']))
return w3
def unpack_uint128(data):
assert data is None or type(data) == bytes, 'data must by bytes, was {}'.format(type(data))
if data and type(data) == bytes:
assert len(data) == 16, 'data must be bytes[16], but was bytes[{}]'.format(len(data))
if data:
return web3.Web3.toInt(data)
else:
return 0
def pack_uint128(value):
assert value is None or (type(value) == int and value >= 0 and value < 2**128)
if value:
data = web3.Web3.toBytes(value)
return b'\x00' * (16 - len(data)) + data
else:
return b'\x00' * 16
def unpack_uint256(data):
assert data is None or type(data) == bytes, 'data must by bytes, was {}'.format(type(data))
if data and type(data) == bytes:
assert len(data) == 32, 'data must be bytes[32], but was bytes[{}]'.format(len(data))
if data:
return int(web3.Web3.toInt(data))
else:
return 0
def pack_uint256(value):
assert value is None or (type(value) == int and value >= 0 and value < 2**256), 'value must be uint256, but was {}'.format(value)
if value:
data = web3.Web3.toBytes(value)
return b'\x00' * (32 - len(data)) + data
else:
return b'\x00' * 32
def pack_ethadr(value: Union[bytes, str], return_dict: bool = False) -> Union[List[int], Dict[str, int]]:
"""
:param value:
:param return_dict:
:return:
"""
if type(value) == str:
if value.startswith('0x'):
value_bytes = a2b_hex(value[2:])
else:
value_bytes = a2b_hex(value)
elif type(value) == bytes:
value_bytes = value
else:
assert False, 'invalid type {} for value'.format(type(value))
assert len(value_bytes) == 20
w = []
for i in range(5):
w.append(struct.unpack('<I', value_bytes[0 + i * 4:4 + i * 4])[0])
if return_dict:
packed_value = {'w0': w[0], 'w1': w[1], 'w2': w[2], 'w3': w[3], 'w4': w[4]}
else:
packed_value = w
return packed_value
def unpack_ethadr(packed_value: Union[List[int], Dict[str, int]], return_str=False) -> Union[bytes, str]:
"""
:param packed_value:
:param return_str:
:return:
"""
w = []
if type(packed_value) == dict:
for i in range(5):
w.append(struct.pack('<I', packed_value['w{}'.format(i)]))
elif type(packed_value) == list:
for i in range(5):
w.append(struct.pack('<I', packed_value[i]))
else:
assert False, 'should not arrive here'
if return_str:
return web3.Web3.toChecksumAddress('0x' + b2a_hex(b''.join(w)).decode())
else:
return b''.join(w)

View File

@@ -0,0 +1,111 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import hashlib
from typing import Optional
import argon2
import hkdf
def stretch_argon2_secret(email: str, password: str, salt: Optional[bytes] = None) -> bytes:
"""
Compute argon2id based secret from user email and password only. This uses Argon2id
for stretching a potentially weak user password/PIN and subsequent HKDF based key
extending to derive private key material (PKM) for different usage contexts.
The Argon2 parameters used are the following:
* kdf ``argon2id-13``
* time cost ``4096``
* memory cost ``512``
* parallelism ``1``
See `draft-irtf-cfrg-argon2 <https://datatracker.ietf.org/doc/draft-irtf-cfrg-argon2/>`__ and
`argon2-cffi <https://argon2-cffi.readthedocs.io/en/stable/>`__.
:param email: User email.
:param password: User password.
:param salt: Optional salt to use (must be 16 bytes long). If none is given, compute salt
from email as ``salt = SHA256(email)[:16]``.
:return: The computed private key material (256b, 32 octets).
"""
if not salt:
m = hashlib.sha256()
m.update(email.encode('utf8'))
salt = m.digest()[:16]
assert len(salt) == 16
pkm = argon2.low_level.hash_secret_raw(
secret=password.encode('utf8'),
salt=salt,
time_cost=4096,
memory_cost=512,
parallelism=1,
hash_len=32,
type=argon2.low_level.Type.ID,
version=19,
)
return pkm
def expand_argon2_secret(pkm: bytes, context: bytes, salt: Optional[bytes] = None) -> bytes:
"""
Expand ``pkm`` and ``context`` into a key of length ``bytes`` using
HKDF's expand function based on HMAC SHA-512). See the HKDF draft RFC and paper for usage notes.
:param pkm:
:param context:
:param salt:
:return:
"""
kdf = hkdf.Hkdf(salt=salt, input_key_material=pkm, hash=hashlib.sha512)
key = kdf.expand(info=context, length=32)
return key
def pkm_from_argon2_secret(email: str, password: str, context: str, salt: Optional[bytes] = None) -> bytes:
"""
:param email:
:param password:
:param context:
:param salt:
:return:
"""
if not salt:
m = hashlib.sha256()
m.update(email.encode('utf8'))
salt = m.digest()[:16]
assert len(salt) == 16
context = context.encode('utf8')
pkm = stretch_argon2_secret(email=email, [PASSWORD-REMOVED], salt=salt)
key = expand_argon2_secret(pkm=pkm, context=context, salt=salt)
return key

View File

@@ -0,0 +1,16 @@
{% if is_first_by_category %}
##
## enum types
##
{% endif%}
class {{ metadata.classname }}(object):
"""
{{ metadata.docs }}
"""
{% for value_name in metadata.values %}
{{ value_name }}: int = {{ metadata.values[value_name].value }}
"""
{{ metadata.values[value_name].docs }}
"""
{% endfor %}

View File

@@ -0,0 +1,5 @@
# Python module "{{ modulename }}"
from . import {{ ', '.join(imports) }}
__all__ = [{{ ', '.join(imports) }}]

View File

@@ -0,0 +1,360 @@
{% if is_first_by_category %}
##
## object types
##
{% endif %}
{% if render_imports %}
import uuid
import pprint
from typing import Dict, List, Optional
from autobahn.wamp.request import Publication, Subscription, Registration
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
{% endif %}
class {{ metadata.classname }}(object):
"""
{{ metadata.docs }}
"""
__slots__ = ['_tab', {% for field in metadata.fields_by_id %}'_{{ field.name }}', {% endfor %}]
def __init__(self, {% for field in metadata.fields_by_id %}{{ field.name }}: {{ field.type.map('python', field.attrs, required=False, objtype_as_string=True) }} = None, {% endfor %}):
# the underlying FlatBuffers vtable
self._tab = None
{% for field in metadata.fields_by_id %}
# {{ field.docs }}
self._{{ field.name }}: {{ field.type.map('python', field.attrs, required=False, objtype_as_string=True) }} = {{ field.name }}
{% endfor %}
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
{% for field in metadata.fields_by_id %}
if other.{{ field.name }} != self.{{ field.name }}:
return False
{% endfor %}
return True
def __ne__(self, other):
return not self.__eq__(other)
{% for field in metadata.fields_by_id %}
@property
def {{ field.name }}(self) -> {{ field.type.map('python', field.attrs, required=False, objtype_as_string=True) }}:
"""
{{ field.docs }}
"""
if self._{{ field.name }} is None and self._tab:
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset({{ field.offset }}))
{% if field.type.map('python', field.attrs, True) == 'str' %}
# access type "string" attribute:
value = ''
if o != 0:
_value = self._tab.String(o + self._tab.Pos)
if _value is not None:
value = _value.decode('utf8')
{% elif field.type.map('python', field.attrs, True) == 'bytes' %}
# access type "bytes" attribute:
value = b''
if o != 0:
_off = self._tab.Vector(o)
_len = self._tab.VectorLen(o)
_value = memoryview(self._tab.Bytes)[_off:_off + _len]
if _value is not None:
value = _value
{% elif field.type.map('python', field.attrs, True) in ['int', 'float', 'double'] %}
# access type "int|float|double" attribute:
value = 0
if o != 0:
_value = self._tab.Get(flatbuffers.number_types.{{ FbsType.FBS2FLAGS[field.type.basetype] }}, o + self._tab.Pos)
if _value is not None:
value = _value
{% elif field.type.map('python', field.attrs, True) == 'bool' %}
# access type "bool" attribute:
value = False
if o != 0:
_value = self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)
if _value is not None:
value = _value
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
# access type "uuid.UUID" attribute:
value = uuid.UUID(bytes=b'\x00' * 16)
if o != 0:
_off = self._tab.Vector(o)
_len = self._tab.VectorLen(o)
_value = memoryview(self._tab.Bytes)[_off:_off + _len]
if _value is not None:
value = uuid.UUID(bytes=bytes(_value))
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
# access type "np.datetime64" attribute:
value = np.datetime64(0, 'ns')
if o != 0:
_value = self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
if value is not None:
value = np.datetime64(_value, 'ns')
{% elif field.type.basetype == FbsType.Vector %}
# access type "Vector" attribute:
value = []
if o != 0:
_start_off = self._tab.Vector(o)
_len = self._tab.VectorLen(o)
for j in range(_len):
_off = _start_off + flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4
_off = self._tab.Indirect(_off)
{% if False and field.type.element == FbsType.Obj %}
_value = {{ field.type.element.split('.')[-1] }}.cast(self._tab.Bytes, _off)
{% else %}
# FIXME [8]
_value = {{ field.type.element }}()
{% endif %}
value.append(_value)
{% elif field.type.basetype == FbsType.Obj %}
# access type "Object" attribute:
{% if field.type.objtype %}
value = {{ field.type.objtype.split('.')[-1] }}()
if o != 0:
_off = self._tab.Indirect(o + self._tab.Pos)
value = {{ field.type.objtype.split('.')[-1] }}.cast(self._tab.Bytes, _off)
{% else %}
# FIXME [9]: objtype of field "{{ field.name }}" is None
value = ''
{% endif %}
{% else %}
# FIXME [5]
raise NotImplementedError('implement processing [5] of FlatBuffers type "{}"'.format({{ field.type.map('python', field.attrs, True) }}))
{% endif %}
assert value is not None
self._{{ field.name }} = value
return self._{{ field.name }}
@{{ field.name }}.setter
def {{ field.name }}(self, value: {{ field.type.map('python', field.attrs, required=False, objtype_as_string=True) }}):
if value is not None:
self._{{ field.name }} = value
else:
{% if field.type.map('python', field.attrs, True) == 'str' %}
# set default value on type "string" attribute:
self._{{ field.name }} = ''
{% elif field.type.map('python', field.attrs, True) == 'bytes' %}
# set default value on type "bytes" attribute:
self._{{ field.name }} = b''
{% elif field.type.map('python', field.attrs, True) in ['int', 'float', 'double'] %}
# set default value on type "int|float|double" attribute:
self._{{ field.name }} = 0
{% elif field.type.map('python', field.attrs, True) == 'bool' %}
# set default value on type "bool" attribute:
self._{{ field.name }} = False
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
# set default value on type "uuid.UUID" attribute:
self._{{ field.name }} = uuid.UUID(bytes=b'\x00' * 16)
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
# set default value on type "np.datetime64" attribute:
self._{{ field.name }} = np.datetime64(0, 'ns')
# set default value on type "List" attribute:
{% elif field.type.basetype == FbsType.Vector %}
self._{{ field.name }} = []
# set default value on type "Object" attribute:
{% elif field.type.basetype == FbsType.Obj %}
self._{{ field.name }} = {{ field.type.map('python', field.attrs, True) }}()
{% else %}
# FIXME [6]
raise NotImplementedError('implement processing [2] of FlatBuffers type "{}", basetype {}'.format({{ field.type.map('python', field.attrs, True) }}, {{ field.type.basetype }}))
{% endif %}
{% endfor %}
@staticmethod
def parse(data: Dict) -> '{{ metadata.classname }}':
"""
Parse generic, native language object into a typed, native language object.
:param data: Generic native language object to parse, e.g. output of ``cbor2.loads``.
:returns: Typed object of this class.
"""
# FIXME
# for key in data.keys():
# assert key in {{ metadata.fields.keys() }}
obj = {{ metadata.classname }}()
{% for field in metadata.fields_by_id %}
if '{{ field.name }}' in data:
{% if field.type.map('python', field.attrs, True) == 'str' %}
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == str), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
obj.{{ field.name }} = data['{{ field.name }}']
{% elif field.type.map('python', field.attrs, True) == 'bytes' %}
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == bytes), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
obj.{{ field.name }} = data['{{ field.name }}']
{% elif field.type.map('python', field.attrs, True) == 'int' %}
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == int), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
obj.{{ field.name }} = data['{{ field.name }}']
{% elif field.type.map('python', field.attrs, True) == 'float' %}
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == float), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
obj.{{ field.name }} = data['{{ field.name }}']
{% elif field.type.map('python', field.attrs, True) == 'bool' %}
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == bool), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
obj.{{ field.name }} = data['{{ field.name }}']
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
assert (data['{{ field.name }}'] is None or (type(data['{{ field.name }}']) == bytes and len(data['{{ field.name }}']) == 16)), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
if data['{{ field.name }}'] is not None:
obj.{{ field.name }} = uuid.UUID(bytes=data['{{ field.name }}'])
else:
obj.{{ field.name }} = None
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == int), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
if data['{{ field.name }}'] is not None:
obj.{{ field.name }} = np.datetime64(data['{{ field.name }}'], 'ns')
else:
obj.{{ field.name }} = np.datetime64(0, 'ns')
{% elif field.type.basetype == FbsType.Vector %}
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == list), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
_value = []
for v in data['{{ field.name }}']:
{% if False and field.type.element == FbsType.Obj %}
# FIXME
_value.append({{ field.type.objtype.split('.')[-1] }}.parse(v))
{% else %}
_value.append(v)
{% endif %}
obj.{{ field.name }} = _value
{% elif field.type.basetype == FbsType.Obj %}
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == dict), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
_value = {{ field.type.map('python', field.attrs, True) }}.parse(data['{{ field.name }}'])
obj.{{ field.name }} = _value
{% else %}
# FIXME [3]
raise NotImplementedError('implement processing [3] of FlatBuffers type "{}"'.format({{ field.type.map('python', field.attrs, True) }}))
{% endif %}
{% endfor %}
return obj
def marshal(self) -> Dict:
"""
Marshal all data contained in this typed native object into a generic object.
:returns: Generic object that can be serialized to bytes using e.g. ``cbor2.dumps``.
"""
obj = {
{% for field in metadata.fields_by_id %}
{% if field.type.map('python', field.attrs, True) in ['str', 'bytes', 'int', 'long', 'float', 'double', 'bool'] %}
'{{ field.name }}': self.{{ field.name }},
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
'{{ field.name }}': self.{{ field.name }}.bytes if self.{{ field.name }} is not None else None,
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
'{{ field.name }}': int(self.{{ field.name }}) if self.{{ field.name }} is not None else None,
{% elif field.type.basetype == FbsType.Vector %}
{% if field.type.element == FbsType.Obj %}
'{{ field.name }}': [o.marshal() for o in self.{{ field.name }}] if self.{{ field.name }} is not None else None,
{% else %}
'{{ field.name }}': self.{{ field.name }},
{% endif %}
{% elif field.type.basetype == FbsType.Obj %}
'{{ field.name }}': self.{{ field.name }}.marshal() if self.{{ field.name }} is not None else None,
{% else %}
# FIXME [4]: implement processing [4] of FlatBuffers type "{{ field.type | string }}" (Python type "{{ field.type.map('python', field.attrs, True) }}")
{% endif %}
{% endfor %}
}
return obj
def __str__(self) -> str:
"""
Return string representation of this object, suitable for e.g. logging.
:returns: String representation of this object.
"""
return '\n{}\n'.format(pprint.pformat(self.marshal()))
@staticmethod
def cast(buf: bytes, offset: int = 0) -> '{{ metadata.classname }}':
"""
Cast a FlatBuffers raw input buffer as a typed object of this class.
:param buf: The raw input buffer to cast.
:param offset: Offset into raw buffer from which to cast flatbuffers from.
:returns: New native object that wraps the FlatBuffers raw buffer.
"""
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = {{ metadata.classname }}()
x._tab = flatbuffers.table.Table(buf, n + offset)
return x
def build(self, builder):
"""
Build a FlatBuffers raw output buffer from this typed object.
:returns: Constructs the FlatBuffers using the builder and
returns ``builder.EndObject()``.
"""
# first, write all string|bytes|etc typed attribute values (in order) to the buffer
{% for field in metadata.fields_by_id %}
{% if field.type.map('python', field.attrs, True) in ['str', 'bytes'] %}
_{{ field.name }} = self.{{ field.name }}
if _{{ field.name }}:
_{{ field.name }} = builder.CreateString(_{{ field.name }})
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
_{{ field.name }} = self.{{ field.name }}.bytes if self.{{ field.name }} else None
if _{{ field.name }}:
_{{ field.name }} = builder.CreateString(_{{ field.name }})
{% else %}
{% endif %}
{% endfor %}
# now start a new object in the buffer and write the actual object attributes (in field
# order) to the buffer
builder.StartObject({{ metadata.fields_by_id|length }})
{% for field in metadata.fields_by_id %}
{% if field.type.map('python', field.attrs, True) in ['str', 'bytes', 'uuid.UUID'] %}
if _{{ field.name }}:
builder.PrependUOffsetTRelativeSlot({{ field.id }}, flatbuffers.number_types.UOffsetTFlags.py_type(_{{ field.name }}), 0)
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
if self.{{ field.name }}:
builder.PrependUint64Slot({{ field.id }}, int(self.{{ field.name }}), 0)
{% elif field.type.map('python', field.attrs, True) in ['bool', 'int', 'float'] %}
if self.{{ field.name }}:
builder.{{ FbsType.FBS2PREPEND[field.type.basetype] }}({{ field.id }}, self.{{ field.name }}, 0)
{% else %}
# FIXME [1]
# raise NotImplementedError('implement builder [1] for type "{}"'.format({{ field.type.map('python', field.attrs, True) }}))
{% endif %}
{% endfor %}
return builder.EndObject()

View File

@@ -0,0 +1,253 @@
{% if is_first_by_category %}
##
## service types (aka "APIs")
##
from uuid import UUID
from typing import List
import txaio
from txaio.interfaces import ILogger
from autobahn.wamp.types import PublishOptions, SubscribeOptions, EventDetails, CallOptions, CallDetails, RegisterOptions
from autobahn.wamp.request import Publication, Subscription, Registration
from autobahn.wamp.interfaces import ISession
from autobahn.xbr import IDelegate
Oid = UUID
Oids = List[UUID]
Void = type(None)
{% endif %}
class {{ metadata.classname }}(object):
"""
{{ metadata.docs }}
Interface UUID: ``{{ metadata.attrs.uuid }}``
"""
__slots__ = [
'log',
'_x_api_id',
'_x_prefix',
'_x_session',
'_x_delegate',
'_x_regs',
'_x_subs',
]
def __init__(self, prefix: str, log: Optional[ILogger]=None):
"""
:param prefix: The URI prefix under which this API will be instantiated under on the realm joined.
:param log: If provided, log to this logger, else create a new one internally.
"""
if log:
self.log = log
else:
import txaio
self.log = txaio.make_logger()
self._x_api_id = uuid.UUID('{{ metadata.attrs.uuid }}')
self._x_prefix = prefix
self._x_session = None
self._x_delegate = None
self._x_regs = None
self._x_subs = None
@property
def api(self) -> uuid.UUID:
"""
Interface UUID of this API (``{{ metadata.attrs.uuid }}``).
"""
return self._x_api_id
@property
def prefix(self) -> str:
"""
WAMP URI prefix under which this API is instantiated.
"""
return self._x_prefix
# WAMP PubSub part of the API:
{% for call_name in metadata.calls_by_id %}
{% if metadata.calls[call_name].attrs['type'] == 'topic' %}
async def publish_{{ call_name }}(self, evt: {{ repo.objs[metadata.calls[call_name].request.name].map('python', required=False, objtype_as_string=True) }}, options: Optional[PublishOptions] = None) -> Optional[Publication]:
"""
As an **interface provider**, publish event:
{{ metadata.calls[call_name].docs }}
:param evt: {{ repo.objs[metadata.calls[call_name].request.name].docs }}
:returns: When doing an acknowledged publish, the WAMP publication is returned.
"""
assert self._x_session and self._x_session.is_attached()
topic = '{}.{{ call_name }}'.format(self._x_prefix)
payload = evt.marshal()
if self._x_delegate:
key_id, enc_ser, ciphertext = await self._x_delegate.wrap(self._x_api_id, topic, payload)
if options.acknowledge:
pub = await self._x_session.publish(topic, key_id, enc_ser, ciphertext, options=options)
else:
self._x_session.publish(topic, key_id, enc_ser, ciphertext, options=options)
pub = None
else:
if options.acknowledge:
pub = await self._x_session.publish(topic, payload, options=options)
else:
self._x_session.publish(topic, payload, options=options)
pub = None
return pub
def receive_{{ call_name }}(self, evt: {{ repo.objs[metadata.calls[call_name].request.name].map('python', required=False, objtype_as_string=True) }}, details: Optional[EventDetails] = None):
"""
As an **interface consumer**, receive event:
{{ metadata.calls[call_name].docs }}
:param evt: {{ repo.objs[metadata.calls[call_name].request.name].docs }}
"""
raise NotImplementedError('event handler for "{{ call_name }}" not implemented')
{% endif %}
{% endfor %}
# WAMP RPC part of the API:
{% for call_name in metadata.calls_by_id %}
{% if metadata.calls[call_name].attrs['type'] == 'procedure' %}
async def call_{{ call_name }}(self, req: {{ repo.objs[metadata.calls[call_name].request.name].map('python', required=False, objtype_as_string=True) }}, options: Optional[CallOptions] = None) -> {{ repo.objs[metadata.calls[call_name].response.name].map('python', required=False, objtype_as_string=True) }}:
"""
As an **interface consumer**, call procedure:
{{ metadata.calls[call_name].docs }}
:param req: {{ repo.objs[metadata.calls[call_name].request.name].docs }}
:returns: {{ repo.objs[metadata.calls[call_name].response.name].docs }}
"""
assert self._x_session and self._x_session.is_attached()
procedure = '{}.{{ call_name }}'.format(self._x_prefix)
payload = req.marshal()
if self._x_delegate:
key_id, enc_ser, ciphertext = await self._x_delegate.wrap(self._x_api_id, procedure, payload)
result = await self._x_session.call(procedure, key_id, enc_ser, ciphertext, options=options)
else:
result = await self._x_session.call(procedure, payload, options=options)
return result
def invoke_{{ call_name }}(self, req: {{ repo.objs[metadata.calls[call_name].request.name].map('python', required=False, objtype_as_string=True) }}, details: Optional[CallDetails] = None) -> {{ repo.objs[metadata.calls[call_name].response.name].map('python', required=False, objtype_as_string=True) }}:
"""
As an **interface provider**, process call invocation:
{{ metadata.calls[call_name].docs }}
:param req: {{ repo.objs[metadata.calls[call_name].request.name].docs }}
:returns: {{ repo.objs[metadata.calls[call_name].response.name].docs }}
"""
raise NotImplementedError('call invocation handler for "{{ call_name }}" not implemented')
{% endif %}
{% endfor %}
@property
def session(self) -> Optional[ISession]:
"""
WAMP session this API is attached to.
"""
return self._x_session
@property
def delegate(self) -> Optional[IDelegate]:
"""
XBR (buyer/seller) delegate this API is attached to.
"""
return self._x_delegate
async def attach(self, session: ISession, delegate: Optional[IDelegate]):
"""
Attach this API instance with the given session and delegate, and under the given WAMP URI prefix.
:param session: WAMP session this API instance is attached to.
:param delegate: If using end-to-end data encryption, XBR ("buyer/seller") delegate used by this API instance.
"""
assert self._x_session is None and session.is_attached()
self._x_session = session
self._x_delegate = delegate
# WAMP PubSub part of the API:
subscriptions = []
{% for call_name in metadata.calls_by_id %}
{% if metadata.calls[call_name].attrs['type'] == 'topic' %}
if self._x_delegate:
async def do_receive_{{ call_name }}(key_id, enc_ser, ciphertext, details=None):
try:
payload = await self._x_delegate.unwrap(key_id, enc_ser, ciphertext)
obj = {{ repo.objs[metadata.calls[call_name].request.name].map('python') }}.parse(payload)
except:
self.log.failure()
else:
self.receive_{{ call_name }}(obj, details=details)
else:
def do_receive_{{ call_name }}(evt, details=None):
obj = {{ repo.objs[metadata.calls[call_name].request.name].map('python') }}.parse(evt)
self.receive_{{ call_name }}(obj, details=details)
topic = '{}.{{ call_name }}'.format(self._x_prefix)
sub = await self._x_session.subscribe(do_receive_{{ call_name }}, topic, options=SubscribeOptions(details=True))
subscriptions.append(sub)
{% endif %}
{% endfor %}
for sub in subscriptions:
self.log.info('Subscription {} created for "{}"'.format(sub.id, sub.topic))
self._x_subs = subscriptions
# WAMP RPC part of the API:
registrations = []
{% for call_name in metadata.calls_by_id %}
{% if metadata.calls[call_name].attrs['type'] == 'procedure' %}
if self._x_delegate:
async def do_invoke_{{ call_name }}(key_id, enc_ser, ciphertext, details=None):
try:
payload = await self._x_delegate.unwrap(key_id, enc_ser, ciphertext)
obj = {{ repo.objs[metadata.calls[call_name].request.name].map('python') }}.parse(payload)
except:
self.log.failure()
else:
self.invoke_{{ call_name }}(obj, details=details)
else:
def do_invoke_{{ call_name }}(req, details=None):
obj = {{ repo.objs[metadata.calls[call_name].request.name].map('python') }}.parse(req)
self.invoke_{{ call_name }}(obj, details=details)
procedure = '{}.{{ call_name }}'.format(self._x_prefix)
reg = await self._x_session.register(do_invoke_{{ call_name }}, procedure, options=RegisterOptions(details=True))
registrations.append(reg)
{% endif %}
{% endfor %}
for reg in registrations:
self.log.info('Registration {} created for "{}"'.format(reg.id, reg.procedure))
self._x_regs = registrations
def detach(self):
"""
Detach this API instance from the session and delegate.
"""
assert self._x_session is not None
dl = []
if self._x_session and self._x_session.is_attached():
for reg in self._x_regs:
dl.append(reg.unregister())
for sub in self._x_subs:
dl.append(sub.unsubscribe())
self._x_session = None
self._x_delegate = None
return txaio.gather(dl, consume_exceptions=True)

View File

@@ -0,0 +1,6 @@
from .{{ metadata.module_relimport }} import {{ metadata.classname }}
def test_{{ metadata.classname }}():
{% for value_name in metadata.values %}
assert {{ metadata.classname }}.{{ value_name }} == {{ metadata.values[value_name].value }}
{% endfor %}

View File

@@ -0,0 +1 @@
# FIXME: add module level unit tests

View File

@@ -0,0 +1,220 @@
{% if render_imports %}
import os
import random
import timeit
import uuid
import cbor2
import txaio
txaio.use_twisted() # noqa
from autobahn import util
from autobahn.wamp.serializer import JsonObjectSerializer, MsgPackObjectSerializer, \
CBORObjectSerializer, UBJSONObjectSerializer
import flatbuffers
import pytest
import numpy as np
from txaio import time_ns
@pytest.fixture(scope='function')
def builder():
_builder = flatbuffers.Builder(0)
return _builder
_SERIALIZERS = [
JsonObjectSerializer(),
MsgPackObjectSerializer(),
CBORObjectSerializer(),
UBJSONObjectSerializer(),
]
{% endif %}
from {{ metadata.module_relimport }} import {{ metadata.classname }}
def fill_{{ metadata.classname }}(obj: {{ metadata.classname }}):
{% if metadata.fields_by_id|length == 0 %}
# class has no fields
pass
{% else %}
{% for field in metadata.fields_by_id %}
{% if field.type.map('python', field.attrs, True) == 'str' %}
obj.{{ field.name }} = util.generate_activation_code()
{% elif field.type.map('python', field.attrs, True) == 'bytes' %}
obj.{{ field.name }} = os.urandom(32)
{% elif field.type.map('python', field.attrs, True) in ['int', 'long'] %}
# FIXME: enum vs int
# obj.{{ field.name }} = random.randint(0, 2**31 - 1)
obj.{{ field.name }} = random.randint(0, 3)
{% elif field.type.map('python', field.attrs, True) in ['float', 'double'] %}
obj.{{ field.name }} = random.random()
{% elif field.type.map('python', field.attrs, True) == 'bool' %}
obj.{{ field.name }} = random.random() > 0.5
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
obj.{{ field.name }} = uuid.uuid4()
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
obj.{{ field.name }} = np.datetime64(time_ns(), 'ns')
{% else %}
obj.{{ field.name }} = None
{% endif %}
{% endfor %}
{% endif %}
def fill_{{ metadata.classname }}_empty(obj: {{ metadata.classname }}):
{% if metadata.fields_by_id|length == 0 %}
# class has no fields
pass
{% else %}
{% for field in metadata.fields_by_id %}
obj.{{ field.name }} = None
{% endfor %}
{% endif %}
@pytest.fixture(scope='function')
def {{ metadata.classname }}_obj():
_obj: {{ metadata.classname }} = {{ metadata.classname }}()
fill_{{ metadata.classname }}(_obj)
return _obj
def test_{{ metadata.classname }}_roundtrip({{ metadata.classname }}_obj, builder):
# serialize to bytes (flatbuffers) from python object
obj = {{ metadata.classname }}_obj.build(builder)
builder.Finish(obj)
data = builder.Output()
# check length of serialized object data
print('{} serialized object length = {} bytes'.format('{{ metadata.classname }}', len(data)))
# create python object from bytes (flatbuffers)
_obj: {{ metadata.classname }} = {{ metadata.classname }}_obj.cast(data)
{% for field in metadata.fields_by_id %}
assert _obj.{{ field.name }} == {{ metadata.classname }}_obj.{{ field.name }}
{% endfor %}
def test_{{ metadata.classname }}_empty(builder):
empty_obj = {{ metadata.classname }}()
fill_{{ metadata.classname }}_empty(empty_obj)
# check the object was initialized correctly
{% for field in metadata.fields_by_id %}
{% if field.type.map('python', field.attrs, True) == 'str' %}
assert empty_obj.{{ field.name }} == ''
{% elif field.type.map('python', field.attrs, True) == 'bytes' %}
assert empty_obj.{{ field.name }} == b''
{% elif field.type.map('python', field.attrs, True) in ['int', 'long'] %}
assert empty_obj.{{ field.name }} == 0
{% elif field.type.map('python', field.attrs, True) in ['float', 'double'] %}
assert empty_obj.{{ field.name }} == 0.0
{% elif field.type.map('python', field.attrs, True) == 'bool' %}
assert empty_obj.{{ field.name }} is False
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
assert empty_obj.{{ field.name }} == uuid.UUID(bytes=b'\0'*16)
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
assert empty_obj.{{ field.name }} == np.datetime64(0, 'ns')
{% else %}
assert empty_obj.{{ field.name }} is None
{% endif %}
{% endfor %}
# serialize to bytes (flatbuffers) from python object
obj = empty_obj.build(builder)
builder.Finish(obj)
data = builder.Output()
# check length of serialized object data
print('{} serialized object length = {} bytes'.format('{{ metadata.classname }}', len(data)))
# create python object from bytes (flatbuffers)
_obj: {{ metadata.classname }} = {{ metadata.classname }}.cast(data)
{% for field in metadata.fields_by_id %}
{% if field.type.map('python', field.attrs, True) == 'str' %}
assert _obj.{{ field.name }} == ''
{% elif field.type.map('python', field.attrs, True) == 'bytes' %}
assert _obj.{{ field.name }} == b''
{% elif field.type.map('python', field.attrs, True) in ['int', 'long'] %}
assert _obj.{{ field.name }} == 0
{% elif field.type.map('python', field.attrs, True) in ['float', 'double'] %}
assert _obj.{{ field.name }} == 0.0
{% elif field.type.map('python', field.attrs, True) == 'bool' %}
assert _obj.{{ field.name }} is False
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
assert _obj.{{ field.name }} == uuid.UUID(bytes=b'\0'*16)
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
assert _obj.{{ field.name }} == np.datetime64(0, 'ns')
{% else %}
assert _obj.{{ field.name }} is None
{% endif %}
{% endfor %}
def test_{{ metadata.classname }}_roundtrip_perf({{ metadata.classname }}_obj, builder):
obj = {{ metadata.classname }}_obj.build(builder)
builder.Finish(obj)
data = builder.Output()
scratch = {'value': 0}
def loop():
_obj: {{ metadata.classname }} = {{ metadata.classname }}.cast(data)
{% for field in metadata.fields_by_id %}
assert _obj.{{ field.name }} == {{ metadata.classname }}_obj.{{ field.name }}
{% endfor %}
scratch['value'] += 1
loop_n = 7
loop_m = 20000
samples = []
print('measuring:')
for i in range(loop_n):
secs = timeit.timeit(loop, number=loop_m)
ops = round(float(loop_m) / secs, 1)
samples.append(ops)
print('{} objects/sec performance'.format(ops))
samples = sorted(samples)
ops50 = samples[int(len(samples) / 2)]
print('RESULT: {} objects/sec median performance'.format(ops50))
assert ops50 > 1000
print(scratch['value'])
def test_{{ metadata.classname }}_marshal_parse({{ metadata.classname }}_obj, builder):
obj = {{ metadata.classname }}_obj.marshal()
_obj = {{ metadata.classname }}_obj.parse(obj)
{% for field in metadata.fields_by_id %}
assert _obj.{{ field.name }} == {{ metadata.classname }}_obj.{{ field.name }}
{% endfor %}
def test_{{ metadata.classname }}_marshal_cbor_parse({{ metadata.classname }}_obj, builder):
obj = {{ metadata.classname }}_obj.marshal()
data = cbor2.dumps(obj)
print('serialized {} to {} bytes (cbor)'.format({{ metadata.classname }}, len(data)))
_obj_raw = cbor2.loads(data)
_obj = {{ metadata.classname }}_obj.parse(_obj_raw)
{% for field in metadata.fields_by_id %}
assert _obj.{{ field.name }} == {{ metadata.classname }}_obj.{{ field.name }}
{% endfor %}
def test_{{ metadata.classname }}_ab_serializer_roundtrip({{ metadata.classname }}_obj, builder):
obj = {{ metadata.classname }}_obj.marshal()
for ser in _SERIALIZERS:
data = ser.serialize(obj)
print('serialized {} to {} bytes ({})'.format({{ metadata.classname }}, len(data), ser.NAME))
msg2 = ser.unserialize(data)[0]
obj2 = {{ metadata.classname }}.parse(msg2)
{% for field in metadata.fields_by_id %}
assert obj2.{{ field.name }} == {{ metadata.classname }}_obj.{{ field.name }}
{% endfor %}

View File

@@ -0,0 +1 @@
# FIXME: add service level unit tests

View File

@@ -0,0 +1,342 @@
{% if is_first_by_category %}
##
## object types
##
{% endif %}
{% if render_imports %}
import uuid
import pprint
from typing import Dict, List, Optional, TypeVar
from autobahn.wamp.request import Publication, Subscription, Registration
import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()
{% endif %}
# https://stackoverflow.com/a/46064289/884770
T_{{ metadata.classname }} = TypeVar('T_{{ metadata.classname }}', bound='{{ metadata.classname }}')
class {{ metadata.classname }}(object):
"""
{{ metadata.docs }}
"""
__slots__ = ['_tab', {% for field_name in metadata.fields_by_id %}'_{{ metadata.fields[field_name].name }}', {% endfor %}]
def __init__(self):
# the underlying FlatBuffers vtable
self._tab = None
{% for field_name in metadata.fields_by_id %}
# {{ metadata.fields[field_name]['docs'] }}
self._{{ metadata.fields[field_name].name }} = None
{% endfor %}
{% for field_name in metadata.fields_by_id %}
@property
def {{ metadata.fields[field_name].name }}(self) -> {{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}:
"""
{{ metadata.fields[field_name]['docs'] }}
"""
if self._{{ metadata.fields[field_name].name }} is None and self._tab:
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset({{ metadata.fields[field_name].offset }}))
{% if metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'str' %}
# access type "string" attribute:
value = ''
if o != 0:
_value = self._tab.String(o + self._tab.Pos)
if _value is not None:
value = _value.decode('utf8')
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'bytes' %}
# access type "bytes" attribute:
value = b''
if o != 0:
_off = self._tab.Vector(o)
_len = self._tab.VectorLen(o)
_value = memoryview(self._tab.Bytes)[_off:_off + _len]
if _value is not None:
value = _value
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) in ['int', 'float', 'double'] %}
# access type "int|float|double" attribute:
value = 0
if o != 0:
_value = self._tab.Get(flatbuffers.number_types.{{ FbsType.FBS2FLAGS[metadata.fields[field_name].type.basetype] }}, o + self._tab.Pos)
if _value is not None:
value = _value
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'bool' %}
# access type "bool" attribute:
value = False
if o != 0:
_value = self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)
if _value is not None:
value = _value
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'uuid.UUID' %}
# access type "uuid.UUID" attribute:
value = uuid.UUID(bytes=b'\x00' * 16)
if o != 0:
_off = self._tab.Vector(o)
_len = self._tab.VectorLen(o)
_value = memoryview(self._tab.Bytes)[_off:_off + _len]
if _value is not None:
value = uuid.UUID(bytes=bytes(_value))
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'np.datetime64' %}
# access type "np.datetime64" attribute:
value = np.datetime64(0, 'ns')
if o != 0:
_value = self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
if value is not None:
value = np.datetime64(_value, 'ns')
{% elif metadata.fields[field_name].type.basetype == FbsType.Vector %}
# access type "Vector" attribute:
value = []
if o != 0:
_start_off = self._tab.Vector(o)
_len = self._tab.VectorLen(o)
for j in range(_len):
_off = _start_off + flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4
_off = self._tab.Indirect(_off)
{% if metadata.fields[field_name].type.element == FbsType.Obj %}
_value = {{ metadata.fields[field_name].type.objtype.split('.')[-1] }}.cast(self._tab.Bytes, _off)
{% else %}
# FIXME [8]
{% endif %}
value.append(_value)
{% elif metadata.fields[field_name].type.basetype == FbsType.Obj %}
# access type "Object" attribute:
value = {{ metadata.fields[field_name].type.objtype.split('.')[-1] }}()
if o != 0:
_off = self._tab.Indirect(o + self._tab.Pos)
value = {{ metadata.fields[field_name].type.objtype.split('.')[-1] }}.cast(self._tab.Bytes, _off)
{% else %}
# FIXME [5]
raise NotImplementedError('implement processing [5] of FlatBuffers type "{}"'.format({{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}))
{% endif %}
assert value is not None
self._{{ metadata.fields[field_name].name }} = value
return self._{{ metadata.fields[field_name].name }}
@{{ metadata.fields[field_name].name }}.setter
def {{ metadata.fields[field_name].name }}(self, value: Optional[{{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}]):
if value is not None:
self._{{ metadata.fields[field_name].name }} = value
else:
{% if metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'str' %}
# set default value on type "string" attribute:
self._{{ metadata.fields[field_name].name }} = ''
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'bytes' %}
# set default value on type "bytes" attribute:
self._{{ metadata.fields[field_name].name }} = b''
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) in ['int', 'float', 'double'] %}
# set default value on type "int|float|double" attribute:
self._{{ metadata.fields[field_name].name }} = 0
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'bool' %}
# set default value on type "bool" attribute:
self._{{ metadata.fields[field_name].name }} = False
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'uuid.UUID' %}
# set default value on type "uuid.UUID" attribute:
self._{{ metadata.fields[field_name].name }} = uuid.UUID(bytes=b'\x00' * 16)
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'np.datetime64' %}
# set default value on type "np.datetime64" attribute:
self._{{ metadata.fields[field_name].name }} = np.datetime64(0, 'ns')
# set default value on type "List" attribute:
{% elif metadata.fields[field_name].type.basetype == FbsType.Vector %}
self._{{ metadata.fields[field_name].name }} = []
# set default value on type "Object" attribute:
{% elif metadata.fields[field_name].type.basetype == FbsType.Obj %}
self._{{ metadata.fields[field_name].name }} = {{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}()
{% else %}
# FIXME [6]
raise NotImplementedError('implement processing [2] of FlatBuffers type "{}", basetype {}'.format({{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}, {{ metadata.fields[field_name].type.basetype }}))
{% endif %}
{% endfor %}
@staticmethod
def parse(data: Dict) -> T_{{ metadata.classname }}:
"""
Parse generic, native language object into a typed, native language object.
:param data: Generic native language object to parse, e.g. output of ``cbor2.loads``.
:returns: Typed object of this class.
"""
for key in data.keys():
assert key in {{ metadata.fields_by_id }}
obj = {{ metadata.classname }}()
{% for field_name in metadata.fields_by_id %}
if '{{ field_name }}' in data:
{% if metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'str' %}
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == str), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
obj.{{ metadata.fields[field_name].name }} = data['{{ field_name }}']
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'bytes' %}
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == bytes), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
obj.{{ metadata.fields[field_name].name }} = data['{{ field_name }}']
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'int' %}
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == int), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
obj.{{ metadata.fields[field_name].name }} = data['{{ field_name }}']
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'float' %}
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == float), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
obj.{{ metadata.fields[field_name].name }} = data['{{ field_name }}']
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'bool' %}
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == bool), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
obj.{{ metadata.fields[field_name].name }} = data['{{ field_name }}']
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'uuid.UUID' %}
assert (data['{{ field_name }}'] is None or (type(data['{{ field_name }}']) == bytes and len(data['{{ field_name }}']) == 16)), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
if data['{{ field_name }}'] is not None:
obj.{{ metadata.fields[field_name].name }} = uuid.UUID(bytes=data['{{ field_name }}'])
else:
obj.{{ metadata.fields[field_name].name }} = None
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'np.datetime64' %}
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == int), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
if data['{{ field_name }}'] is not None:
obj.{{ metadata.fields[field_name].name }} = np.datetime64(data['{{ field_name }}'], 'ns')
else:
obj.{{ metadata.fields[field_name].name }} = np.datetime64(0, 'ns')
{% elif metadata.fields[field_name].type.basetype == FbsType.Vector %}
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == list), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
_value = []
for v in data['{{ field_name }}']:
{% if metadata.fields[field_name].type.element == FbsType.Obj %}
_value.append({{ metadata.fields[field_name].type.objtype.split('.')[-1] }}.parse(v))
{% else %}
_value.append(v)
{% endif %}
obj.{{ metadata.fields[field_name].name }} = _value
{% elif metadata.fields[field_name].type.basetype == FbsType.Obj %}
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == dict), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
_value = {{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}.parse(data['{{ field_name }}'])
obj.{{ metadata.fields[field_name].name }} = _value
{% else %}
# FIXME [3]
raise NotImplementedError('implement processing [3] of FlatBuffers type "{}"'.format({{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}))
{% endif %}
{% endfor %}
return obj
def marshal(self) -> Dict:
"""
Marshal all data contained in this typed native object into a generic object.
:returns: Generic object that can be serialized to bytes using e.g. ``cbor2.dumps``.
"""
obj = {
{% for field_name in metadata.fields_by_id %}
{% if metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) in ['str', 'bytes', 'int', 'long', 'float', 'double', 'bool'] %}
'{{ metadata.fields[field_name].name }}': self.{{ metadata.fields[field_name].name }},
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'uuid.UUID' %}
'{{ metadata.fields[field_name].name }}': self.{{ metadata.fields[field_name].name }}.bytes if self.{{ metadata.fields[field_name].name }} is not None else None,
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'np.datetime64' %}
'{{ metadata.fields[field_name].name }}': int(self.{{ metadata.fields[field_name].name }}) if self.{{ metadata.fields[field_name].name }} is not None else None,
{% elif metadata.fields[field_name].type.basetype == FbsType.Vector %}
{% if metadata.fields[field_name].type.element == FbsType.Obj %}
'{{ metadata.fields[field_name].name }}': [o.marshal() for o in self.{{ metadata.fields[field_name].name }}] if self.{{ metadata.fields[field_name].name }} is not None else None,
{% else %}
'{{ metadata.fields[field_name].name }}': self.{{ metadata.fields[field_name].name }},
{% endif %}
{% elif metadata.fields[field_name].type.basetype == FbsType.Obj %}
'{{ metadata.fields[field_name].name }}': self.{{ metadata.fields[field_name].name }}.marshal() if self.{{ metadata.fields[field_name].name }} is not None else None,
{% else %}
# FIXME [4]: implement processing [4] of FlatBuffers type "{{ metadata.fields[field_name].type | string }}" (Python type "{{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}")
{% endif %}
{% endfor %}
}
return obj
def __str__(self) -> str:
"""
Return string representation of this object, suitable for e.g. logging.
:returns: String representation of this object.
"""
return '\n{}\n'.format(pprint.pformat(self.marshal()))
@staticmethod
def cast(buf: bytes, offset: int = 0) -> T_{{ metadata.classname }}:
"""
Cast a FlatBuffers raw input buffer as a typed object of this class.
:param buf: The raw input buffer to cast.
:param offset: Offset into raw buffer from which to cast flatbuffers from.
:returns: New native object that wraps the FlatBuffers raw buffer.
"""
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
x = {{ metadata.classname }}()
x._tab = flatbuffers.table.Table(buf, n + offset)
return x
def build(self, builder):
"""
Build a FlatBuffers raw output buffer from this typed object.
:returns: Constructs the FlatBuffers using the builder and
returns ``builder.EndObject()``.
"""
# first, write all string|bytes|etc typed attribute values (in order) to the buffer
{% for field_name in metadata.fields_by_id %}
{% if metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) in ['str', 'bytes'] %}
_{{ field_name }} = self.{{ field_name }}
if _{{ field_name }}:
_{{ field_name }} = builder.CreateString(_{{ field_name }})
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'uuid.UUID' %}
_{{ field_name }} = self.{{ field_name }}.bytes if self.{{ field_name }} else None
if _{{ field_name }}:
_{{ field_name }} = builder.CreateString(_{{ field_name }})
{% else %}
{% endif %}
{% endfor %}
# now start a new object in the buffer and write the actual object attributes (in field
# order) to the buffer
builder.StartObject({{ metadata.fields_by_id|length }})
{% for field_name in metadata.fields_by_id %}
{% if metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) in ['str', 'bytes', 'uuid.UUID'] %}
if _{{ field_name }}:
builder.PrependUOffsetTRelativeSlot({{ metadata.fields[field_name].id }}, flatbuffers.number_types.UOffsetTFlags.py_type(_{{ field_name }}), 0)
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'np.datetime64' %}
if self.{{ field_name }}:
builder.PrependUint64Slot({{ metadata.fields[field_name].id }}, int(self.{{ field_name }}), 0)
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) in ['bool', 'int', 'float'] %}
if self.{{ field_name }}:
builder.{{ FbsType.FBS2PREPEND[metadata.fields[field_name].type.basetype] }}({{ metadata.fields[field_name].id }}, self.{{ field_name }}, 0)
{% else %}
# FIXME [1]
# raise NotImplementedError('implement builder [1] for type "{}"'.format({{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}))
{% endif %}
{% endfor %}
return builder.EndObject()

View File

@@ -0,0 +1,25 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

View File

@@ -0,0 +1,5 @@
[default]
url=ws://localhost:9000/ws
privkey=default.priv
pubkey=default.pub

View File

@@ -0,0 +1,9 @@
Crossbar.io user private key - KEEP THIS SAFE!
creator: oberstet@intel-nuci7
created-at: 2022-05-06T14:40:09.639Z
user-id: oberstet@intel-nuci7
public-key-ed25519: [AWS-SECRET-REMOVED]546f90eb5123667935d2f561
public-adr-eth: [AWS-SECRET-REMOVED]9c
private-key-ed25519: [AWS-SECRET-REMOVED]b314583d0c8d8a4942f9be40
private-key-eth: [AWS-SECRET-REMOVED]3dc4a1cfd3a489ea387c496b

View File

@@ -0,0 +1,7 @@
Crossbar.io user public key
creator: oberstet@intel-nuci7
created-at: 2022-05-06T14:40:09.639Z
user-id: oberstet@intel-nuci7
public-key-ed25519: [AWS-SECRET-REMOVED]546f90eb5123667935d2f561
public-adr-eth: [AWS-SECRET-REMOVED]9c

View File

@@ -0,0 +1,70 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import unittest
from binascii import a2b_hex
from autobahn.xbr import HAS_XBR
TESTVECTORS = [
{
'email': 'foobar@example.com',
'password': 'secret123',
'salt': None,
'pkm': '[AWS-SECRET-REMOVED]bc8c135a8c17b6b5e3686cab',
'contexts': {
'wamp-cryptosign': '[AWS-SECRET-REMOVED]377d2617104dd5622a9209cf',
}
},
{
'email': 'foobar@example.com',
'password': 'secret123',
'salt': a2b_hex('3761e806cda3c35d859c933d46e5d57b'),
'pkm': '[AWS-SECRET-REMOVED]cb27738a849a1cd5bfcf4bed',
'contexts': {
'wamp-cryptosign': '[AWS-SECRET-REMOVED]1bffb00461edfebbaffcda71',
}
},
]
if HAS_XBR:
from autobahn.xbr import stretch_argon2_secret, pkm_from_argon2_secret
class TestXbrArgon2(unittest.TestCase):
def test_stretch_argon2_secret(self):
for tv in TESTVECTORS:
email, password, salt = tv['email'], tv['password'], tv['salt']
pkm = stretch_argon2_secret(email, password, salt=salt)
self.assertEqual(pkm, a2b_hex(tv['pkm']))
def test_pkm_from_argon2_secret(self):
for tv in TESTVECTORS:
email, password, salt = tv['email'], tv['password'], tv['salt']
for context, expected_priv_key in tv['contexts'].items():
expected_priv_key = a2b_hex(expected_priv_key)
priv_key = pkm_from_argon2_secret(email=email, [PASSWORD-REMOVED], context=context, salt=salt)
self.assertEqual(priv_key, expected_priv_key)

View File

@@ -0,0 +1,78 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
import unittest
from autobahn.xbr import HAS_XBR
if HAS_XBR:
from autobahn.xbr import Profile, UserConfig
class TestXbrUserConfig(unittest.TestCase):
DOTDIR = '~/.xbrnetwork'
PROFILE_NAME = 'default'
NETWORK_URL = 'wss://planet.xbr.network/ws'
NETWORK_REALM = 'xbrnetwork'
PASSWORD = 'secret123'
def test_create_empty_config(self):
c = UserConfig('config.ini')
self.assertEqual(c.profiles, {})
def test_create_empty_profile(self):
p = Profile()
self.assertTrue(p.path is None)
def test_load_home(self):
config_dir = os.path.expanduser(self.DOTDIR)
if not os.path.isdir(config_dir):
os.mkdir(config_dir)
config_path = os.path.join(config_dir, 'config.ini')
if os.path.exists(config_path):
c = UserConfig(config_path)
c.load()
self.assertIn(self.PROFILE_NAME, c.profiles)
def test_write_default_config(self):
config_dir = os.path.expanduser(self.DOTDIR)
if not os.path.isdir(config_dir):
os.mkdir(config_dir)
config_path = os.path.join(config_dir, 'test.ini')
c = UserConfig(config_path)
p = Profile()
c.profiles[self.PROFILE_NAME] = p
c.save(self.PASSWORD)
c2 = UserConfig(config_path)
def get_pw():
return self.PASSWORD
c2.load(cb_get_password=get_pw)
self.assertIn(self.PROFILE_NAME, c2.profiles)

View File

@@ -0,0 +1,511 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
import sys
import tempfile
from binascii import a2b_hex, b2a_hex
from unittest import skipIf
from twisted.internet.defer import inlineCallbacks
from twisted.trial.unittest import TestCase
from autobahn.wamp.cryptosign import HAS_CRYPTOSIGN
from autobahn.xbr import HAS_XBR
if HAS_XBR and HAS_CRYPTOSIGN:
from autobahn.wamp.cryptosign import CryptosignKey
from autobahn.xbr import make_w3, EthereumKey
from autobahn.xbr._secmod import SecurityModuleMemory
from autobahn.xbr import create_eip712_delegate_certificate, create_eip712_authority_certificate
from autobahn.xbr._eip712_delegate_certificate import EIP712DelegateCertificate
from autobahn.xbr._eip712_authority_certificate import EIP712AuthorityCertificate
from autobahn.xbr._eip712_certificate_chain import parse_certificate_chain
# https://web3py.readthedocs.io/en/stable/providers.html#infura-mainnet
HAS_INFURA = 'WEB3_INFURA_PROJECT_ID' in os***REMOVED***iron and len(os***REMOVED***iron['WEB3_INFURA_PROJECT_ID']) > 0
# TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary
IS_CPY_310 = sys.version_info.minor == 10
@skipIf(not os***REMOVED***iron.get('USE_TWISTED', False), 'only for Twisted')
@skipIf(not HAS_INFURA, 'env var WEB3_INFURA_PROJECT_ID not defined')
@skipIf(not (HAS_XBR and HAS_CRYPTOSIGN), 'package autobahn[encryption,xbr] not installed')
class TestEip712Certificate(TestCase):
def setUp(self):
self._gw_config = {
'type': 'infura',
'key': os***REMOVED***iron.get('WEB3_INFURA_PROJECT_ID', ''),
'network': 'mainnet',
}
self._w3 = make_w3(self._gw_config)
self._seedphrase = "avocado style uncover thrive same grace crunch want essay reduce current edge"
self._sm: SecurityModuleMemory = SecurityModuleMemory.from_seedphrase(self._seedphrase, num_eth_keys=5,
num_cs_keys=5)
@inlineCallbacks
def test_eip712_delegate_certificate(self):
yield self._sm.open()
delegate_eth_key: EthereumKey = self._sm[1]
delegate_cs_key: CryptosignKey = self._sm[6]
chainId = 1
verifyingContract = a2b_hex('[AWS-SECRET-REMOVED]57'[2:])
validFrom = 15124128
delegate = delegate_eth_key.address(binary=True)
csPubKey = delegate_cs_key.public_key(binary=True)
bootedAt = 1657579546469365046 # txaio.time_ns()
meta = '[AWS-SECRET-REMOVED]DKEoiu'
cert_data = create_eip712_delegate_certificate(chainId=chainId, verifyingContract=verifyingContract,
validFrom=validFrom, delegate=delegate, csPubKey=csPubKey,
bootedAt=bootedAt, meta=meta)
# print('\n\n{}\n\n'.format(pformat(cert_data)))
cert_sig = yield delegate_eth_key.sign_typed_data(cert_data, binary=False)
self.assertEqual(cert_sig,
'[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]529eb1d11b')
yield self._sm.close()
@inlineCallbacks
def test_eip712_authority_certificate(self):
yield self._sm.open()
trustroot_eth_key: EthereumKey = self._sm[0]
delegate_eth_key: EthereumKey = self._sm[1]
chainId = 1
verifyingContract = a2b_hex('[AWS-SECRET-REMOVED]57'[2:])
validFrom = 15124128
issuer = trustroot_eth_key.address(binary=True)
subject = delegate_eth_key.address(binary=True)
realm = a2b_hex('[AWS-SECRET-REMOVED]96'[2:])
capabilities = 3
meta = '[AWS-SECRET-REMOVED]DKEoiu'
cert_data = create_eip712_authority_certificate(chainId=chainId, verifyingContract=verifyingContract,
validFrom=validFrom, issuer=issuer, subject=subject,
realm=realm, capabilities=capabilities, meta=meta)
# print('\n\n{}\n\n'.format(pformat(cert_data)))
cert_sig = yield trustroot_eth_key.sign_typed_data(cert_data, binary=False)
self.assertEqual(cert_sig,
'[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]1bf977b61c')
yield self._sm.close()
@skipIf(not os***REMOVED***iron.get('USE_TWISTED', False), 'only for Twisted')
@skipIf(not HAS_INFURA, 'env var WEB3_INFURA_PROJECT_ID not defined')
@skipIf(not (HAS_XBR and HAS_CRYPTOSIGN), 'package autobahn[encryption,xbr] not installed')
class TestEip712CertificateChain(TestCase):
def setUp(self):
self._gw_config = {
'type': 'infura',
'key': os***REMOVED***iron.get('WEB3_INFURA_PROJECT_ID', ''),
'network': 'mainnet',
}
self._w3 = make_w3(self._gw_config)
self._seedphrase = "avocado style uncover thrive same grace crunch want essay reduce current edge"
self._sm: SecurityModuleMemory = SecurityModuleMemory.from_seedphrase(self._seedphrase, num_eth_keys=5,
num_cs_keys=5)
# HELLO.Details.authextra.certificates
#
self._certs_expected1 = [(None,
{'domain': {'name': 'WMP', 'version': '1'},
'message': {'bootedAt': 1657781999086394759,
'chainId': 1,
'csPubKey': '[AWS-SECRET-REMOVED]063701cd9c4011a777d04089',
'delegate': '[AWS-SECRET-REMOVED]F6',
'meta': '[AWS-SECRET-REMOVED]DKEoiu',
'validFrom': 15139218,
'verifyingContract': '[AWS-SECRET-REMOVED]57'},
'primaryType': 'EIP712DelegateCertificate',
'types': {'EIP712DelegateCertificate': [{'name': 'chainId',
'type': 'uint256'},
{'name': 'verifyingContract',
'type': 'address'},
{'name': 'validFrom',
'type': 'uint256'},
{'name': 'delegate',
'type': 'address'},
{'name': 'csPubKey',
'type': 'bytes32'},
{'name': 'bootedAt',
'type': 'uint64'},
{'name': 'meta', 'type': 'string'}],
'EIP712Domain': [{'name': 'name', 'type': 'string'},
{'name': 'version', 'type': 'string'}]}},
'[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]1cfa7e191c'),
(None,
{'domain': {'name': 'WMP', 'version': '1'},
'message': {'capabilities': 12,
'chainId': 1,
'issuer': '[AWS-SECRET-REMOVED]57',
'meta': '[AWS-SECRET-REMOVED]V4zC3G',
'realm': '[AWS-SECRET-REMOVED]96',
'subject': '[AWS-SECRET-REMOVED]F6',
'validFrom': 15139218,
'verifyingContract': '[AWS-SECRET-REMOVED]57'},
'primaryType': 'EIP712AuthorityCertificate',
'types': {'EIP712AuthorityCertificate': [{'name': 'chainId',
'type': 'uint256'},
{'name': 'verifyingContract',
'type': 'address'},
{'name': 'validFrom',
'type': 'uint256'},
{'name': 'issuer',
'type': 'address'},
{'name': 'subject',
'type': 'address'},
{'name': 'realm',
'type': 'address'},
{'name': 'capabilities',
'type': 'uint64'},
{'name': 'meta', 'type': 'string'}],
'EIP712Domain': [{'name': 'name', 'type': 'string'},
{'name': 'version', 'type': 'string'}]}},
'[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]1069bd841c'),
(None,
{'domain': {'name': 'WMP', 'version': '1'},
'message': {'capabilities': 63,
'chainId': 1,
'issuer': '[AWS-SECRET-REMOVED]57',
'meta': '[AWS-SECRET-REMOVED]V4zC3G',
'realm': '[AWS-SECRET-REMOVED]96',
'subject': '[AWS-SECRET-REMOVED]57',
'validFrom': 15139218,
'verifyingContract': '[AWS-SECRET-REMOVED]57'},
'primaryType': 'EIP712AuthorityCertificate',
'types': {'EIP712AuthorityCertificate': [{'name': 'chainId',
'type': 'uint256'},
{'name': 'verifyingContract',
'type': 'address'},
{'name': 'validFrom',
'type': 'uint256'},
{'name': 'issuer',
'type': 'address'},
{'name': 'subject',
'type': 'address'},
{'name': 'realm',
'type': 'address'},
{'name': 'capabilities',
'type': 'uint64'},
{'name': 'meta', 'type': 'string'}],
'EIP712Domain': [{'name': 'name', 'type': 'string'},
{'name': 'version', 'type': 'string'}]}},
'[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]7b91bebe1b')]
@skipIf(True, 'FIXME: builtins.TypeError: to_checksum_address() takes 1 positional argument but 2 were given')
@inlineCallbacks
def test_eip712_create_certificate_chain_manual(self):
yield self._sm.open()
# keys needed to create all certificates in certificate chain
#
trustroot_eth_key: EthereumKey = self._sm[0]
delegate_eth_key: EthereumKey = self._sm[1]
delegate_cs_key: CryptosignKey = self._sm[6]
# data needed for delegate certificate: cert1
#
chainId = 1 # self._w3.eth.chain_id
verifyingContract = a2b_hex('[AWS-SECRET-REMOVED]57'[2:])
validFrom = 15139218 # self._w3.eth.block_number
delegate = delegate_eth_key.address(binary=True)
csPubKey = delegate_cs_key.public_key(binary=True)
bootedAt = 1657781999086394759 # txaio.time_ns()
delegateMeta = '[AWS-SECRET-REMOVED]DKEoiu'
# data needed for intermediate authority certificate: cert2
#
issuer_cert2 = trustroot_eth_key.address(binary=True)
subject_cert2 = delegate
realm_cert2 = a2b_hex('[AWS-SECRET-REMOVED]96'[2:])
capabilities_cert2 = EIP712AuthorityCertificate.CAPABILITY_PUBLIC_RELAY | EIP712AuthorityCertificate.CAPABILITY_PRIVATE_RELAY
meta_cert2 = '[AWS-SECRET-REMOVED]V4zC3G'
# data needed for root authority certificate: cert3
#
issuer_cert3 = trustroot_eth_key.address(binary=True)
subject_cert3 = issuer_cert3
realm_cert3 = a2b_hex('[AWS-SECRET-REMOVED]96'[2:])
capabilities_cert3 = EIP712AuthorityCertificate.CAPABILITY_ROOT_CA | EIP712AuthorityCertificate.CAPABILITY_INTERMEDIATE_CA | EIP712AuthorityCertificate.CAPABILITY_PUBLIC_RELAY | EIP712AuthorityCertificate.CAPABILITY_PRIVATE_RELAY | EIP712AuthorityCertificate.CAPABILITY_PROVIDER | EIP712AuthorityCertificate.CAPABILITY_CONSUMER
meta_cert3 = '[AWS-SECRET-REMOVED]V4zC3G'
# FIXME: builtins.TypeError: to_checksum_address() takes 1 positional argument but 2 were given
# create delegate certificate
#
cert1_data = create_eip712_delegate_certificate(chainId=chainId, verifyingContract=verifyingContract,
validFrom=validFrom, delegate=delegate, csPubKey=csPubKey,
bootedAt=bootedAt, meta=delegateMeta)
cert1_sig = yield delegate_eth_key.sign_typed_data(cert1_data, binary=False)
cert1_data['message']['csPubKey'] = b2a_hex(cert1_data['message']['csPubKey']).decode()
cert1_data['message']['delegate'] = self._w3.toChecksumAddress(cert1_data['message']['delegate'])
cert1_data['message']['verifyingContract'] = self._w3.toChecksumAddress(
cert1_data['message']['verifyingContract'])
# create intermediate authority certificate
#
cert2_data = create_eip712_authority_certificate(chainId=chainId, verifyingContract=verifyingContract,
validFrom=validFrom, issuer=issuer_cert2,
subject=subject_cert2,
realm=realm_cert2, capabilities=capabilities_cert2,
meta=meta_cert2)
cert2_sig = yield trustroot_eth_key.sign_typed_data(cert2_data, binary=False)
cert2_data['message']['verifyingContract'] = self._w3.toChecksumAddress(
cert2_data['message']['verifyingContract'])
cert2_data['message']['issuer'] = self._w3.toChecksumAddress(cert2_data['message']['issuer'])
cert2_data['message']['subject'] = self._w3.toChecksumAddress(cert2_data['message']['subject'])
cert2_data['message']['realm'] = self._w3.toChecksumAddress(cert2_data['message']['realm'])
# create root authority certificate
#
cert3_data = create_eip712_authority_certificate(chainId=chainId, verifyingContract=verifyingContract,
validFrom=validFrom, issuer=issuer_cert3,
subject=subject_cert3,
realm=realm_cert3, capabilities=capabilities_cert3,
meta=meta_cert3)
cert3_sig = yield trustroot_eth_key.sign_typed_data(cert3_data, binary=False)
cert3_data['message']['verifyingContract'] = self._w3.toChecksumAddress(
cert3_data['message']['verifyingContract'])
cert3_data['message']['issuer'] = self._w3.toChecksumAddress(cert3_data['message']['issuer'])
cert3_data['message']['subject'] = self._w3.toChecksumAddress(cert3_data['message']['subject'])
cert3_data['message']['realm'] = self._w3.toChecksumAddress(cert3_data['message']['realm'])
# create certificates chain
#
certificates = [(None, cert1_data, cert1_sig), (None, cert2_data, cert2_sig), (None, cert3_data, cert3_sig)]
if False:
from pprint import pprint
print()
pprint(certificates)
print()
# check certificates and certificate signatures of whole chain
#
self.assertEqual(certificates, self._certs_expected1)
yield self._sm.close()
@inlineCallbacks
def test_eip712_create_certificate_chain_highlevel(self):
yield self._sm.open()
# keys needed to create all certificates in certificate chain
ca_key: EthereumKey = self._sm[0]
# data needed for root authority certificate: cert3
ca_cert_chainId = 1
ca_cert_verifyingContract = a2b_hex('[AWS-SECRET-REMOVED]57'[2:])
ca_cert_validFrom = 666666
ca_cert_issuer = ca_key.address(binary=True)
ca_cert_subject = ca_cert_issuer
ca_cert_realm = a2b_hex('[AWS-SECRET-REMOVED]96'[2:])
ca_cert_capabilities = EIP712AuthorityCertificate.CAPABILITY_ROOT_CA | EIP712AuthorityCertificate.CAPABILITY_INTERMEDIATE_CA | EIP712AuthorityCertificate.CAPABILITY_PUBLIC_RELAY | EIP712AuthorityCertificate.CAPABILITY_PRIVATE_RELAY | EIP712AuthorityCertificate.CAPABILITY_PROVIDER | EIP712AuthorityCertificate.CAPABILITY_CONSUMER
ca_cert_meta = ''
# create root authority certificate signature: directly from provided data attributes
ca_cert_data = create_eip712_authority_certificate(chainId=ca_cert_chainId,
verifyingContract=ca_cert_verifyingContract,
validFrom=ca_cert_validFrom, issuer=ca_cert_issuer,
subject=ca_cert_subject, realm=ca_cert_realm,
capabilities=ca_cert_capabilities, meta=ca_cert_meta)
ca_cert_sig = yield ca_key.sign_typed_data(ca_cert_data, binary=False)
# create root authority certificate signature: from certificate object
ca_cert = EIP712AuthorityCertificate(chainId=ca_cert_chainId,
verifyingContract=ca_cert_verifyingContract,
validFrom=ca_cert_validFrom,
issuer=ca_cert_issuer,
subject=ca_cert_subject,
realm=ca_cert_realm,
capabilities=ca_cert_capabilities,
meta=ca_cert_meta)
ca_cert_sig2 = yield ca_cert.sign(ca_key)
# re-create root authority certificate from round-tripping (marshal-parse)
ca_cert2 = EIP712AuthorityCertificate.parse(ca_cert.marshal())
ca_cert_sig3 = yield ca_cert2.sign(ca_key)
# all different ways to compute signature must result in same signature value
self.assertEqual(ca_cert_sig, ca_cert_sig2)
self.assertEqual(ca_cert_sig, ca_cert_sig3)
# and match this signature value
self.assertEqual(ca_cert_sig, '[AWS-SECRET-REMOVED]1acbe11bf6210f3a48a56de830aa6a566cc4920'
'[AWS-SECRET-REMOVED]d5a75f4131c')
# test save/load instance to/from file
with tempfile.NamedTemporaryFile() as fd:
# save certificate to file
ca_cert.save(fd.name)
# load certificate from file
ca_cert3 = EIP712AuthorityCertificate.load(fd.name)
# ensure it produces the same signature
ca_cert_sig4 = yield ca_cert3.sign(ca_key)
self.assertEqual(ca_cert_sig, ca_cert_sig4)
yield self._sm.close()
@inlineCallbacks
def test_eip712_verify_certificate_chain_manual(self):
yield self._sm.open()
# keys originally used to sign the certificates in the certificate chain
trustroot_eth_key: EthereumKey = self._sm[0]
delegate_eth_key: EthereumKey = self._sm[1]
delegate_cs_key: CryptosignKey = self._sm[6]
# parse the whole certificate chain
cert_chain = []
cert_sigs = []
for cert_hash, cert_data, cert_sig in self._certs_expected1:
self.assertIn('domain', cert_data)
self.assertIn('message', cert_data)
self.assertIn('primaryType', cert_data)
self.assertIn('types', cert_data)
self.assertIn(cert_data['primaryType'], cert_data['types'])
self.assertIn(cert_data['primaryType'], ['EIP712DelegateCertificate', 'EIP712AuthorityCertificate'])
if cert_data['primaryType'] == 'EIP712DelegateCertificate':
cert = EIP712DelegateCertificate.parse(cert_data)
elif cert_data['primaryType'] == 'EIP712AuthorityCertificate':
cert = EIP712AuthorityCertificate.parse(cert_data)
else:
assert False, 'should not arrive here'
cert_chain.append(cert)
cert_sigs.append(cert_sig)
# FIXME: allow length 2 and length > 3
self.assertEqual(len(cert_chain), 3)
self.assertEqual(cert_chain[0].delegate, delegate_eth_key.address(binary=True))
self.assertEqual(cert_chain[0].csPubKey, delegate_cs_key.public_key(binary=True))
self.assertEqual(cert_chain[1].issuer, trustroot_eth_key.address(binary=True))
self.assertEqual(cert_chain[2].issuer, trustroot_eth_key.address(binary=True))
# Certificate Chain Rules (CCR):
#
# 1. **CCR-1**: The `chainId` and `verifyingContract` must match for all certificates to what we expect, and `validFrom` before current block number on the respective chain.
# 2. **CCR-2**: The `realm` must match for all certificates to the respective realm.
# 3. **CCR-3**: The type of the first certificate in the chain must be a `EIP712DelegateCertificate`, and all subsequent certificates must be of type `EIP712AuthorityCertificate`.
# 4. **CCR-4**: The last certificate must be self-signed (`issuer` equals `subject`), it is a root CA certificate.
# 5. **CCR-5**: The intermediate certificate's `issuer` must be equal to the `subject` of the previous certificate.
# 6. **CCR-6**: The root certificate must be `validFrom` before the intermediate certificate
# 7. **CCR-7**: The `capabilities` of intermediate certificate must be a subset of the root cert
# 8. **CCR-8**: The intermediate certificate's `subject` must be the delegate certificate `delegate`
# 9. **CCR-9**: The intermediate certificate must be `validFrom` before the delegate certificate
# 10. **CCR-10**: The root certificate's signature must be valid and signed by the root certificate's `issuer`.
# 11. **CCR-11**: The intermediate certificate's signature must be valid and signed by the intermediate certificate's `issuer`.
# 12. **CCR-12**: The delegate certificate's signature must be valid and signed by the `delegate`.
# CCR-3
self.assertIsInstance(cert_chain[0], EIP712DelegateCertificate)
for i in [1, len(cert_chain) - 1]:
self.assertIsInstance(cert_chain[i], EIP712AuthorityCertificate)
# CCR-1
chainId = cert_chain[2].chainId
verifyingContract = cert_chain[2].verifyingContract
for cert in cert_chain:
self.assertEqual(cert.chainId, chainId)
self.assertEqual(cert.verifyingContract, verifyingContract)
# CCR-2
realm = cert_chain[2].realm
for cert in cert_chain[1:]:
self.assertEqual(cert.realm, realm)
# CCR-4
self.assertEqual(cert_chain[2].subject, cert_chain[2].issuer)
# CCR-5
self.assertEqual(cert_chain[1].issuer, cert_chain[2].subject)
# CCR-6
self.assertLessEqual(cert_chain[2].validFrom, cert_chain[1].validFrom)
# CCR-7
self.assertTrue(cert_chain[2].capabilities == cert_chain[2].capabilities | cert_chain[1].capabilities)
# CCR-8
self.assertEqual(cert_chain[1].subject, cert_chain[0].delegate)
# CCR-9
self.assertLessEqual(cert_chain[1].validFrom, cert_chain[0].validFrom)
# CCR-10
_issuer = cert_chain[2].recover(a2b_hex(cert_sigs[2]))
self.assertEqual(_issuer, trustroot_eth_key.address(binary=True))
# CCR-11
_issuer = cert_chain[1].recover(a2b_hex(cert_sigs[1]))
self.assertEqual(_issuer, trustroot_eth_key.address(binary=True))
# CCR-12
_issuer = cert_chain[0].recover(a2b_hex(cert_sigs[0]))
self.assertEqual(_issuer, delegate_eth_key.address(binary=True))
yield self._sm.close()
@inlineCallbacks
def test_eip712_verify_certificate_chain_highlevel(self):
yield self._sm.open()
# keys originally used to sign the certificates in the certificate chain
trustroot_eth_key: EthereumKey = self._sm[0]
delegate_eth_key: EthereumKey = self._sm[1]
delegate_cs_key: CryptosignKey = self._sm[6]
certificates = parse_certificate_chain(self._certs_expected1)
self.assertEqual(certificates[2].issuer, trustroot_eth_key.address(binary=True))
self.assertEqual(certificates[0].delegate, delegate_eth_key.address(binary=True))
self.assertEqual(certificates[0].csPubKey, delegate_cs_key.public_key(binary=True))
yield self._sm.close()

View File

@@ -0,0 +1,183 @@
import os
import sys
from unittest import skipIf
from unittest.mock import MagicMock
from twisted.trial.unittest import TestCase
from twisted.internet.defer import inlineCallbacks
from autobahn.xbr import HAS_XBR
from autobahn.wamp.cryptosign import HAS_CRYPTOSIGN
if HAS_XBR and HAS_CRYPTOSIGN:
from autobahn.xbr._frealm import Seeder, FederatedRealm
from autobahn.xbr._secmod import SecurityModuleMemory, EthereumKey
from autobahn.wamp.cryptosign import CryptosignKey
# https://web3py.readthedocs.io/en/stable/providers.html#infura-mainnet
HAS_INFURA = 'WEB3_INFURA_PROJECT_ID' in os***REMOVED***iron and len(os***REMOVED***iron['WEB3_INFURA_PROJECT_ID']) > 0
# TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary
IS_CPY_310 = sys.version_info.minor == 10
@skipIf(not os***REMOVED***iron.get('USE_TWISTED', False), 'only for Twisted')
@skipIf(not HAS_INFURA, 'env var WEB3_INFURA_PROJECT_ID not defined')
@skipIf(not (HAS_XBR and HAS_CRYPTOSIGN), 'package autobahn[encryption,xbr] not installed')
class TestFederatedRealm(TestCase):
gw_config = {
'type': 'infura',
'key': os***REMOVED***iron.get('WEB3_INFURA_PROJECT_ID', ''),
'network': 'mainnet',
}
# "builtins.TypeError: As of 3.10, the *loop* parameter was removed from Lock() since
# it is no longer necessary"
#
# solved via websockets>=10.3, but web3==5.29.0 requires websockets<10
#
@skipIf(IS_CPY_310, 'Web3 v5.29.0 (web3.auto.infura) raises TypeError on Python 3.10')
def test_frealm_ctor_auto(self):
name = 'wamp-proto.eth'
fr = FederatedRealm(name)
self.assertEqual(fr.status, 'STOPPED')
self.assertEqual(fr.name_or_address, name)
self.assertEqual(fr.gateway_config, None)
self.assertEqual(fr.name_category, 'ens')
def test_frealm_ctor_gw(self):
name = 'wamp-proto.eth'
fr = FederatedRealm(name, self.gw_config)
self.assertEqual(fr.status, 'STOPPED')
self.assertEqual(fr.name_or_address, name)
self.assertEqual(fr.gateway_config, self.gw_config)
self.assertEqual(fr.name_category, 'ens')
@inlineCallbacks
def test_frealm_initialize(self):
name = 'wamp-proto.eth'
fr1 = FederatedRealm(name, self.gw_config)
self.assertEqual(fr1.status, 'STOPPED')
yield fr1.initialize()
self.assertEqual(fr1.status, 'RUNNING')
self.assertEqual(fr1.address, '[AWS-SECRET-REMOVED]4a')
def test_frealm_seeders(self):
fr1 = MagicMock()
fr1.name_or_address = 'wamp-proto.eth'
fr1.address = '[AWS-SECRET-REMOVED]4a'
fr1.status = 'RUNNING'
fr1.seeders = [
Seeder(frealm=fr1,
endpoint='wss://frealm1.example.com/ws',
label='Example Inc.',
operator='[AWS-SECRET-REMOVED]bC',
country='US'),
Seeder(frealm=fr1,
endpoint='wss://fr1.foobar.org/ws',
label='Foobar Foundation',
operator='[AWS-SECRET-REMOVED]9c',
country='DE'),
Seeder(frealm=fr1,
endpoint='wss://public-frealm1.pierre.fr:443',
label='Pierre PP',
operator='[AWS-SECRET-REMOVED]2B',
country='FR'),
]
self.assertEqual(len(fr1.seeders), 3)
transports = [s.endpoint for s in fr1.seeders]
self.assertEqual(transports, ['wss://frealm1.example.com/ws', 'wss://fr1.foobar.org/ws',
'wss://public-frealm1.pierre.fr:443'])
@inlineCallbacks
def test_frealm_secmod(self):
name = 'wamp-proto.eth'
seedphrase = "myth like bonus scare over problem client lizard pioneer submit female collect"
sm = SecurityModuleMemory.from_seedphrase(seedphrase)
yield sm.open()
self.assertEqual(len(sm), 2)
self.assertTrue(isinstance(sm[0], EthereumKey), 'unexpected type {} at index 0'.format(type(sm[0])))
self.assertTrue(isinstance(sm[1], CryptosignKey), 'unexpected type {} at index 1'.format(type(sm[1])))
fr = FederatedRealm(name, self.gw_config)
# FIXME
fr._seeders = [
Seeder(frealm=fr,
endpoint='wss://frealm1.example.com/ws',
label='Example Inc.',
operator='[AWS-SECRET-REMOVED]bC',
country='US'),
Seeder(frealm=fr,
endpoint='wss://fr1.foobar.org/ws',
label='Foobar Foundation',
operator='[AWS-SECRET-REMOVED]9c',
country='DE'),
Seeder(frealm=fr,
endpoint='wss://public-frealm1.pierre.fr:443',
label='Pierre PP',
operator='[AWS-SECRET-REMOVED]2B',
country='FR'),
]
yield fr.initialize()
self.assertEqual(fr.status, 'RUNNING')
self.assertEqual(fr.address, '[AWS-SECRET-REMOVED]4a')
self.assertEqual(len(fr.seeders), 3)
delegate_key = sm[0]
client_key = sm[1]
authextra = yield fr.seeders[0].create_authextra(client_key=client_key,
delegate_key=delegate_key,
bandwidth_requested=512,
channel_id=None,
channel_binding=None)
self.assertEqual(authextra.get('pubkey', None), client_key.public_key(binary=False))
# print(authextra)
self.assertTrue('signature' in authextra)
self.assertTrue(type(authextra['signature']) == str)
self.assertEqual(len(authextra['signature']), 65 * 2)
# @skipIf(not os***REMOVED***iron.get('WAMP_ROUTER_URLS', None), 'WAMP_ROUTER_URLS not defined')
# @skipIf(not os***REMOVED***iron.get('USE_TWISTED', False), 'only for Twisted')
# @skipIf(not HAS_XBR, 'package autobahn[xbr] not installed')
# class TestFederatedRealmNetworked(TestCase):
#
# def test_seeders_multi_reconnect(self):
# from autobahn.twisted.component import Component, run
#
# # WAMP_ROUTER_URLS=ws://localhost:8080/ws,ws://localhost:8081/ws,ws://localhost:8082/ws
# # crossbar start --cbdir=./autobahn/xbr/test/.crossbar --config=config1.json
# transports = os***REMOVED***iron.get('WAMP_ROUTER_URLS', '').split(',')
# realm = 'realm1'
# authentication = {
# 'cryptosign': {
# 'privkey': '[AWS-SECRET-REMOVED]b314583d0c8d8a4942f9be40',
# }
# }
#
# component = Component(transports=transports, realm=realm, authentication=authentication)
# # component.start()
#
# # @inlineCallbacks
# # def main(reactor, session):
# # print("Client session={}".format(session))
# # res = yield session.call('user.add2', 23, 666)
# # print(res)
# # session.leave()
# #
# # from autobahn.wamp.component import _run
# # from twisted.internet import reactor
# # d = _run(reactor, [component])
# # #d = run([component], log_level='info', stop_at_close=True)
# # res = yield d

View File

@@ -0,0 +1,83 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
from autobahn.xbr import HAS_XBR
if HAS_XBR:
import unittest
import binascii
from autobahn.xbr import generate_seedphrase, check_seedphrase, account_from_seedphrase
_SEEDPHRASE = "myth like bonus scare over problem client lizard pioneer submit female collect"
_INVALID_SEEDPHRASE = "9 nn \0 kk$"
_EXPECTED = [
('[AWS-SECRET-REMOVED]C1', '[AWS-SECRET-REMOVED]13bce9c46f30d7d21715b23b1d'),
('[AWS-SECRET-REMOVED]f0', '[AWS-SECRET-REMOVED]c4e6134f022a85b1ffdd59b2a1'),
('[AWS-SECRET-REMOVED]2b', '[AWS-SECRET-REMOVED]daa446c22ee2d73db3707e620c'),
('[AWS-SECRET-REMOVED]2d', '[AWS-SECRET-REMOVED]6029e2fd9fc169899c440a7913'),
('[AWS-SECRET-REMOVED]17', '[AWS-SECRET-REMOVED]d9bc938c843a79e7b4fd2ad743'),
('[AWS-SECRET-REMOVED]BC', '[AWS-SECRET-REMOVED]b79c53d7dd6a3536a33ab8a4fd'),
('[AWS-SECRET-REMOVED]A9', '[AWS-SECRET-REMOVED]5fa577fc294ebd14db90767a52'),
('[AWS-SECRET-REMOVED]5E', '[AWS-SECRET-REMOVED]a94bfde4d19872c44cf65386e3'),
('[AWS-SECRET-REMOVED]6E', '[AWS-SECRET-REMOVED]29d6e75613329ca6f1d31c0bb4'),
('[AWS-SECRET-REMOVED]6e', '[AWS-SECRET-REMOVED]97b5bada8d7711f758981c3773'),
('[AWS-SECRET-REMOVED]60', '[AWS-SECRET-REMOVED]3cc9212911efd35dff0373153f'),
('[AWS-SECRET-REMOVED]4A', '[AWS-SECRET-REMOVED]3276c1cdc0de1f98cefee81c01'),
('[AWS-SECRET-REMOVED]43', '[AWS-SECRET-REMOVED]8781883a535d2941f66db07b24'),
('[AWS-SECRET-REMOVED]E6', '[AWS-SECRET-REMOVED]c5900c0691af75f1a8a52c8268'),
('[AWS-SECRET-REMOVED]98', '[AWS-SECRET-REMOVED]e2798a11255a46f533852dfe46'),
('[AWS-SECRET-REMOVED]Be', '[AWS-SECRET-REMOVED]10fc998b37d219de19c39ee58c'),
('[AWS-SECRET-REMOVED]a3', '[AWS-SECRET-REMOVED]14b3359b64f8b66565679f7299'),
('[AWS-SECRET-REMOVED]E3', '[AWS-SECRET-REMOVED]a2533e7910755d15403a0749e8'),
('[AWS-SECRET-REMOVED]3A', '[AWS-SECRET-REMOVED]fd6d10fe1ae87c5c4bcd6ba491'),
('[AWS-SECRET-REMOVED]95', '[AWS-SECRET-REMOVED]0b881faae7af08e567e58915bd'),
]
class TestEthereumMnemonic(unittest.TestCase):
def test_check_valid_seedphrase(self):
self.assertTrue(check_seedphrase(_SEEDPHRASE))
def test_check_invalid_seedphrase(self):
self.assertFalse(check_seedphrase(_INVALID_SEEDPHRASE))
def test_generate_seedphrase(self):
for strength in [128, 160, 192, 224, 256]:
seedphrase = generate_seedphrase(strength)
self.assertEqual(type(seedphrase), str)
for word in seedphrase.split():
self.assertTrue(type(word) == str)
self.assertTrue(check_seedphrase(seedphrase))
def test_derive_wallet(self):
for i, (public_adr, private_key) in enumerate(_EXPECTED):
account = account_from_seedphrase(_SEEDPHRASE, i)
private_key = binascii.a2b_hex(private_key[2:])
self.assertEqual(account.address, public_adr)
self.assertEqual(account.key, private_key)

View File

@@ -0,0 +1,103 @@
import os
import copy
import pkg_resources
from random import randint, random
import txaio
from unittest import skipIf
if 'USE_TWISTED' in os***REMOVED***iron and os***REMOVED***iron['USE_TWISTED']:
from twisted.trial import unittest
txaio.use_twisted()
else:
import unittest
txaio.use_asyncio()
from autobahn.xbr import HAS_XBR
from autobahn.wamp.exception import InvalidPayload
if HAS_XBR:
from autobahn.xbr import FbsRepository
@skipIf(not HAS_XBR, 'package autobahn[xbr] not installed')
class TestFbsBase(unittest.TestCase):
"""
FlatBuffers tests base class, loads test schemas.
"""
def setUp(self):
self.repo = FbsRepository('autobahn')
self.archives = []
for fbs_file in ['demo.bfbs', 'wamp-control.bfbs']:
archive = pkg_resources.resource_filename('autobahn', 'xbr/test/catalog/schema/{}'.format(fbs_file))
self.repo.load(archive)
self.archives.append(archive)
class TestFbsValidateTestTableA(TestFbsBase):
def test_validate_TestTableA_valid(self):
valid_args = [
True,
randint(-127, -1),
randint(1, 255),
randint(-2 ** 15, -1),
randint(1, 2 ** 16 - 1),
randint(-2 ** 31, -1),
randint(1, 2 ** 32 - 1),
randint(-2 ** 63, -1),
randint(1, 2 ** 64 - 1),
2.0 + random(),
2.0 + random(),
]
try:
self.repo.validate('demo.TestTableA', args=valid_args, kwargs={})
except Exception as exc:
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
def test_validate_TestTableA_invalid(self):
valid_args = [
True,
randint(-127, -1),
randint(1, 255),
randint(-2 ** 15, -1),
randint(1, 2 ** 16 - 1),
randint(-2 ** 31, -1),
randint(1, 2 ** 32 - 1),
randint(-2 ** 63, -1),
randint(1, 2 ** 64 - 1),
2.0 + random(),
2.0 + random(),
]
# mandatory field with wrong type
for i in range(len(valid_args)):
# copy valid value, and set one column to a value of wrong type
invalid_args = copy.copy(valid_args)
if i == 0:
# first column should be bool, so make it invalid with an int value
invalid_args[0] = 666
else:
# all other columns are something different from bool, so make it invalid with a bool value
invalid_args[i] = True
self.assertRaisesRegex(InvalidPayload, 'invalid type', self.repo.validate,
'demo.TestTableA', invalid_args, {})
# mandatory field with wrong type `None`
if True:
for i in range(len(valid_args)):
# copy valid value, and set one column to a value of wrong type
invalid_args = copy.copy(valid_args)
invalid_args[i] = None
self.assertRaisesRegex(InvalidPayload, 'invalid type', self.repo.validate,
'demo.TestTableA', invalid_args, {})
# mandatory field missing
if True:
for i in range(len(valid_args)):
invalid_args = valid_args[:i]
self.assertRaisesRegex(InvalidPayload, 'missing positional argument', self.repo.validate,
'demo.TestTableA', invalid_args, {})

View File

@@ -0,0 +1,325 @@
import os
import pkg_resources
from binascii import a2b_hex
import txaio
from unittest import skipIf
if 'USE_TWISTED' in os***REMOVED***iron and os***REMOVED***iron['USE_TWISTED']:
from twisted.trial import unittest
txaio.use_twisted()
else:
import unittest
txaio.use_asyncio()
from autobahn.xbr import HAS_XBR
from autobahn.wamp.exception import InvalidPayload
if HAS_XBR:
from autobahn.xbr._util import pack_ethadr, unpack_ethadr
from autobahn.xbr import FbsType, FbsObject, FbsService, FbsRPCCall, FbsRepository, FbsSchema, FbsField, FbsEnum, \
FbsEnumValue
@skipIf(not HAS_XBR, 'package autobahn[xbr] not installed')
class TestPackEthAdr(unittest.TestCase):
"""
Test :func:`pack_ethadr` and :func:`unpack_ethadr` helpers.
"""
def test_roundtrip(self):
original_value_str = '[AWS-SECRET-REMOVED]47'
original_value_bin = a2b_hex(original_value_str[2:])
# count number of test cases run
cnt = 0
# test 2 cases for return_dict option
for return_dict in [False, True]:
# test 2 cases for input type
for original_value in [original_value_str, original_value_bin]:
packed_value = pack_ethadr(original_value, return_dict=return_dict)
if return_dict:
self.assertIsInstance(packed_value, dict)
for i in range(5):
self.assertIn('w{}'.format(i), packed_value)
self.assertTrue(type(packed_value['w{}'.format(i)]) == int)
else:
self.assertIsInstance(packed_value, list)
self.assertEqual(len(packed_value), 5)
for i in range(5):
self.assertTrue(type(packed_value[i]) == int)
# test 2 cases for return_str option
for return_str in [False, True]:
unpacked_value = unpack_ethadr(packed_value, return_str=return_str)
if return_str:
self.assertIsInstance(unpacked_value, str)
self.assertEqual(unpacked_value, original_value_str)
else:
self.assertIsInstance(unpacked_value, bytes)
self.assertEqual(unpacked_value, original_value_bin)
cnt += 1
# assure we actually completed as many test cases as we expect
self.assertEqual(cnt, 8)
@skipIf(not HAS_XBR, 'package autobahn[xbr] not installed')
class TestFbsBase(unittest.TestCase):
"""
FlatBuffers tests base class, loads test schemas.
"""
def setUp(self):
self.repo = FbsRepository('autobahn')
self.archives = []
for fbs_file in ['wamp.bfbs', 'testsvc1.bfbs']:
archive = pkg_resources.resource_filename('autobahn', 'xbr/test/catalog/schema/{}'.format(fbs_file))
self.repo.load(archive)
self.archives.append(archive)
class TestFbsRepository(TestFbsBase):
"""
Test :class:`FbsRepository` schema loading and verify loaded types.
"""
def test_create_from_archive(self):
self.assertIn('uint160_t', self.repo.objs)
self.assertIsInstance(self.repo.objs['uint160_t'], FbsObject)
self.assertIn('testsvc1.TestRequest', self.repo.objs)
self.assertIsInstance(self.repo.objs['testsvc1.TestRequest'], FbsObject)
self.assertIn('testsvc1.TestResponse', self.repo.objs)
self.assertIsInstance(self.repo.objs['testsvc1.TestResponse'], FbsObject)
self.assertIn('testsvc1.ITestService1', self.repo.services)
self.assertIsInstance(self.repo.services['testsvc1.ITestService1'], FbsService)
def test_loaded_schema(self):
schema_fn = pkg_resources.resource_filename('autobahn', 'xbr/test/catalog/schema/testsvc1.bfbs')
# get reflection schema loaded
schema: FbsSchema = self.repo.schemas[schema_fn]
# get call from service defined in schema
call: FbsRPCCall = schema.services['testsvc1.ITestService1'].calls['run_something1']
# for each of the call request and call response type names ...
call_type: FbsObject
for call_type in [schema.objs[call.request.name], schema.objs[call.response.name]]:
# ... iterate over all fields
field: FbsField
for field in call_type.fields_by_id:
# we only need to process the "_type" fields auto-added for Union types
if field.type.basetype == FbsType.UType:
assert field.name.endswith('_type')
# get the enum storing the Union
call_type_enum: FbsEnum = schema.enums_by_id[field.type.index]
assert call_type_enum.is_union
# get all enum values, which store Union types
union_type_value: FbsEnumValue
for union_type_value in call_type_enum.values:
if union_type_value != 'NONE':
# resolve union type value names in same namespace as containing union type [???]
if '.' in call_type_enum.name:
namespace = call_type_enum.name.split('.')[0]
union_type_qn = '{}.{}'.format(namespace, union_type_value)
else:
union_type_qn = union_type_value
# get type object for Union type by fully qualified name
union_type = schema.objs[union_type_qn]
print(union_type)
# print(self.repo.objs['testsvc1.TestRequest'])
# print(self.repo.enums['testsvc1.TestRequestAny'])
# print(self.repo.objs['testsvc1.TestRequestArgument'])
# print(self.repo.objs['testsvc1.TestRequestProgress'])
# print()
# print(self.repo.objs['testsvc1.TestResponse'])
# print(self.repo.enums['testsvc1.TestResponseAny'])
# print(self.repo.objs['testsvc1.TestResponseResult'])
# print(self.repo.objs['testsvc1.TestResponseProgress'])
# print(self.repo.objs['testsvc1.TestResponseError1'])
# print(self.repo.objs['testsvc1.TestResponseError2'])
# print()
# svc1 = self.repo.services['testsvc1.ITestService1']
# for key in svc1.calls.keys():
# ep: FbsRPCCall = svc1.calls[key]
# print(ep)
# svc2 = self.repo.services['trading.ITradingClock']
# for key in svc2.calls.keys():
# ep: FbsRPCCall = svc2.calls[key]
# print(ep)
class TestFbsValidateUint160(TestFbsBase):
"""
Test struct uint160_t validation.
"""
def test_validate_obj_uint160_valid(self):
element_max = 2 ** 32 - 1
valid_values = [
[0, 0, 0, 0, 0],
[element_max, element_max, element_max, element_max, element_max],
pack_ethadr('[AWS-SECRET-REMOVED]00'),
pack_ethadr('[AWS-SECRET-REMOVED]47'),
{'w0': 0, 'w1': 0, 'w2': 0, 'w3': 0, 'w4': 0},
{'w0': element_max, 'w1': element_max, 'w2': element_max, 'w3': element_max, 'w4': element_max},
pack_ethadr('[AWS-SECRET-REMOVED]00', return_dict=True),
pack_ethadr('[AWS-SECRET-REMOVED]47', return_dict=True),
]
try:
for value in valid_values:
self.repo.validate_obj('uint160_t', value)
except Exception as exc:
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
def test_validate_obj_uint160_invalid(self):
tests = [
(None, 'invalid type'),
([], 'missing argument'),
({}, 'missing argument'),
([0, 0, None, 0, 0], 'invalid type'),
([0, 0, 0, 0, 'bogus'], 'invalid type'),
([0, 0, 0, 0], 'missing argument'),
([0, 0, 0, 0, 0, 0], 'unexpected argument'),
({'w0': 0, 'w1': 0, 'w2': None, 'w3': 0, 'w4': 0}, 'invalid type'),
({'w0': 0, 'w1': 0, 'w2': 0, 'w3': 0, 'w4': 'bogus'}, 'invalid type'),
({'w0': 0, 'w1': 0, 'w2': 0, 'w3': 0}, 'missing argument'),
({'w0': 0, 'w1': 0, 'w2': 0, 'w3': 0, 'w4': 0, 'w5': 0}, 'unexpected argument'),
]
for value, expected_regex in tests:
self.assertRaisesRegex(InvalidPayload, expected_regex,
self.repo.validate_obj, 'uint160_t', value)
class TestFbsValidateEthAddress(TestFbsBase):
def test_validate_obj_EthAddress_valid(self):
for value in [
{'value': {'w0': 0, 'w1': 0, 'w2': 0, 'w3': 0, 'w4': 0}},
{'value': pack_ethadr('[AWS-SECRET-REMOVED]00')},
{'value': pack_ethadr('[AWS-SECRET-REMOVED]47')},
{'value': pack_ethadr('[AWS-SECRET-REMOVED]00', return_dict=True)},
{'value': pack_ethadr('[AWS-SECRET-REMOVED]47', return_dict=True)},
]:
try:
self.repo.validate_obj('EthAddress', value)
except Exception as exc:
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
def test_validate_obj_EthAddress_invalid(self):
tests = [
# FIXME
# (None, 'invalid type'),
# ([], 'invalid type'),
({'invalid_key': pack_ethadr('[AWS-SECRET-REMOVED]47')}, 'unexpected argument'),
({'value': None}, 'invalid type'),
({'value': {}}, 'missing argument'),
({'value': []}, 'missing argument'),
]
for value, expected_regex in tests:
self.assertRaisesRegex(InvalidPayload, expected_regex,
self.repo.validate_obj, 'EthAddress', value)
class TestFbsValidateKeyValue(TestFbsBase):
def test_validate_KeyValue_valid(self):
try:
self.repo.validate('KeyValue', args=['foo', '23'], kwargs={})
except Exception as exc:
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
def test_validate_KeyValue_invalid(self):
self.assertRaisesRegex(InvalidPayload, 'missing positional argument', self.repo.validate,
'KeyValue', [], {})
self.assertRaisesRegex(InvalidPayload, 'missing positional argument', self.repo.validate,
'KeyValue', ['foo'], {})
self.assertRaisesRegex(InvalidPayload, 'unexpected positional arguments', self.repo.validate,
'KeyValue', ['foo', '23', 'unexpected'], {})
self.assertRaisesRegex(InvalidPayload, 'unexpected keyword arguments', self.repo.validate,
'KeyValue', ['foo', '23'], {'unexpected_kwarg': '23'})
self.assertRaisesRegex(InvalidPayload, 'invalid type', self.repo.validate,
'KeyValue', ['foo', 23], {})
def test_validate_KeyValues_valid(self):
# empty list
valid_value = {}
try:
self.repo.validate_obj('KeyValues', valid_value)
except Exception as exc:
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
# non-empty list
valid_value = {
'value': []
}
for i in range(10):
# valid_value['value'].append(['key{}'.format(i), 'value{}'.format(i)])
valid_value['value'].append({'key': 'key{}'.format(i), 'value': 'value{}'.format(i)})
try:
self.repo.validate_obj('KeyValues', valid_value)
except Exception as exc:
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
def test_validate_KeyValues_invalid(self):
tests = [
(None, 'invalid type'),
([], 'invalid type'),
({'invalid_key': 'something'}, 'unexpected argument'),
({'value': None}, 'invalid type'),
({'value': {}}, 'invalid type'),
]
for value, expected_regex in tests:
self.assertRaisesRegex(InvalidPayload, expected_regex,
self.repo.validate_obj, 'KeyValues', value)
class TestFbsValidateVoid(TestFbsBase):
def test_validate_Void_valid(self):
try:
self.repo.validate(None, args=[], kwargs={})
self.repo.validate('Void', args=[], kwargs={})
except Exception as exc:
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
def test_validate_Void_invalid(self):
valid_adr = pack_ethadr('[AWS-SECRET-REMOVED]47')
self.assertRaisesRegex(InvalidPayload, 'unexpected positional argument', self.repo.validate,
'Void', [23], {})
self.assertRaisesRegex(InvalidPayload, 'unexpected positional argument', self.repo.validate,
'Void', [{}], {})
self.assertRaisesRegex(InvalidPayload, 'unexpected positional argument', self.repo.validate,
'Void', [None], {})
self.assertRaisesRegex(InvalidPayload, 'unexpected positional argument', self.repo.validate,
'Void', [{'value': valid_adr}], {})
self.assertRaisesRegex(InvalidPayload, 'unexpected keyword argument', self.repo.validate,
'Void', [], {'unexpected_kwarg': None})
self.assertRaisesRegex(InvalidPayload, 'unexpected keyword argument', self.repo.validate,
'Void', [], {'unexpected_kwarg': 23})

View File

@@ -0,0 +1,224 @@
import os
import copy
import pkg_resources
import txaio
from unittest import skipIf
if 'USE_TWISTED' in os***REMOVED***iron and os***REMOVED***iron['USE_TWISTED']:
from twisted.trial import unittest
txaio.use_twisted()
else:
import unittest
txaio.use_asyncio()
from autobahn.xbr import HAS_XBR
from autobahn.wamp.exception import InvalidPayload
if HAS_XBR:
from autobahn.xbr import FbsRepository
@skipIf(not HAS_XBR, 'package autobahn[xbr] not installed')
class TestFbsBase(unittest.TestCase):
"""
FlatBuffers tests base class, loads test schemas.
"""
def setUp(self):
self.repo = FbsRepository('autobahn')
self.archives = []
for fbs_file in ['wamp-control.bfbs']:
archive = pkg_resources.resource_filename('autobahn', 'xbr/test/catalog/schema/{}'.format(fbs_file))
self.repo.load(archive)
self.archives.append(archive)
class TestFbsValidatePermissionAllow(TestFbsBase):
def test_validate_PermissionAllow_valid(self):
tests = [
{
'call': True,
'register': True,
'publish': True,
'subscribe': True
},
{
'call': False,
'register': False,
'publish': False,
'subscribe': False
},
]
for value in tests:
try:
self.repo.validate_obj('wamp.PermissionAllow', value)
except Exception as exc:
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
def test_validate_PermissionAllow_invalid(self):
tests = [
(None, 'invalid type'),
(666, 'invalid type'),
(True, 'invalid type'),
({'some_unexpected_key': 666}, 'unexpected argument'),
({'call': True, 'register': True, 'publish': True}, 'missing argument'),
({'call': True, 'register': True, 'publish': True, 'subscribe': 666}, 'invalid type'),
({'call': True, 'register': True, 'publish': True, 'subscribe': None}, 'invalid type'),
({'call': True, 'register': True, 'publish': True, 'subscribe': True, 'some_unexpected_key': 666},
'unexpected argument'),
]
for value, expected_regex in tests:
self.assertRaisesRegex(InvalidPayload, expected_regex,
self.repo.validate_obj, 'wamp.PermissionAllow', value)
class TestFbsValidateRolePermission(TestFbsBase):
def test_validate_RolePermission_valid(self):
tests = [
{},
{
'uri': 'com.example.',
'match': 'prefix',
'allow': {
'call': True,
'register': True,
'publish': True,
'subscribe': True
},
'disclose': {
'caller': True,
'publisher': True,
},
'cache': True
},
]
for value in tests:
try:
self.repo.validate_obj('wamp.RolePermission', value)
except Exception as exc:
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
def test_validate_RolePermission_invalid(self):
tests = [
(None, 'invalid type'),
({'some_unexpected_key': True}, 'unexpected argument'),
({'uri': 'com.example.', 'allow': {'some_unexpected_key': True}}, 'unexpected argument'),
({'uri': 666}, 'invalid type'),
({'uri': 'com.example.', 'match': 'prefix', 'allow': {'call': 666}}, 'invalid type'),
({'uri': 666, 'match': 'prefix',
'allow': {'call': True, 'register': True, 'publish': True, 'subscribe': True},
'disclose': {'caller': True, 'publisher': True}, 'cache': True}, 'invalid type'),
]
for value, expected_regex in tests:
self.assertRaisesRegex(InvalidPayload, expected_regex,
self.repo.validate_obj, 'wamp.RolePermission', value)
class TestFbsValidateRoleConfig(TestFbsBase):
def setUp(self):
super().setUp()
self.role_config1 = {
"name": "anonymous",
"permissions": [{
"uri": "",
"match": "prefix",
"allow": {
"call": True,
"register": True,
"publish": True,
"subscribe": True
},
"disclose": {
"caller": True,
"publisher": True
},
"cache": True
}]
}
def test_RoleConfig_valid(self):
try:
self.repo.validate_obj('wamp.RoleConfig', self.role_config1)
except Exception as exc:
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
def test_RoleConfig_invalid(self):
config = copy.copy(self.role_config1)
config['name'] = 666
self.assertRaisesRegex(InvalidPayload, 'invalid type', self.repo.validate_obj,
'wamp.RoleConfig', config)
# config = copy.copy(self.realm_config1)
# del config['roles']
# config['foobar'] = 666
# self.assertRaisesRegex(InvalidPayload, 'missing positional argument', self.repo.validate_obj,
# 'wamp.RealmConfig', config)
class TestFbsValidateRealmConfig(TestFbsBase):
def setUp(self):
super().setUp()
self.realm_config1 = {
"name": "realm1",
"roles": [{
"name": "anonymous",
"permissions": [{
"uri": "",
"match": "prefix",
"allow": {
"call": True,
"register": True,
"publish": True,
"subscribe": True
},
"disclose": {
"caller": True,
"publisher": True
},
"cache": True
}]
}]
}
def test_RealmConfig_valid(self):
try:
self.repo.validate_obj('wamp.RealmConfig', self.realm_config1)
except Exception as exc:
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
def test_RealmConfig_invalid(self):
config = copy.copy(self.realm_config1)
config['name'] = 666
self.assertRaisesRegex(InvalidPayload, 'invalid type', self.repo.validate_obj,
'wamp.RealmConfig', config)
def test_start_router_realm_valid(self):
valid_args = ['realm023', self.realm_config1]
try:
self.repo.validate('wamp.StartRealm', args=valid_args, kwargs={})
except Exception as exc:
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
def test_start_router_realm_invalid(self):
tests = [
(None, None, 'missing positional argument'),
(None, {}, 'missing positional argument'),
(['realm023', {}], {'bogus': 666}, 'unexpected keyword arguments'),
([], None, 'missing positional argument'),
(['realm023'], None, 'missing positional argument'),
(['realm023', None], None, 'invalid type'),
(['realm023', 666], None, 'invalid type'),
(['realm023', {'name': 'realm1', 'bogus': []}], None, 'unexpected argument'),
(['realm023', {'name': 666}], None, 'invalid type'),
(['realm023', {'name': 'realm1', 'roles': 666}], None, 'invalid type'),
(['realm023', {'name': 'realm1', 'roles': None}], None, 'invalid type'),
(['realm023', {'name': 'realm1', 'roles': {}}], None, 'invalid type'),
(['realm023', {'name': 'realm1', 'roles': [{'name': 666}]}], None, 'invalid type'),
]
for args, kwargs, expected_regex in tests:
self.assertRaisesRegex(InvalidPayload, expected_regex,
self.repo.validate, 'wamp.StartRealm', args=args, kwargs=kwargs)

View File

@@ -0,0 +1,459 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
import sys
import pkg_resources
from random import randint, random
from binascii import a2b_hex
from typing import List
from unittest import skipIf
from twisted.internet.defer import inlineCallbacks
from twisted.trial.unittest import TestCase
from autobahn.wamp.cryptosign import HAS_CRYPTOSIGN
from autobahn.xbr import HAS_XBR
if HAS_XBR and HAS_CRYPTOSIGN:
from py_eth_sig_utils.eip712 import encode_typed_data
from py_eth_sig_utils.utils import ecsign, ecrecover_to_pub, checksum_encode, sha3
from py_eth_sig_utils.signing import v_r_s_to_signature, signature_to_v_r_s
from py_eth_sig_utils.signing import sign_typed_data, recover_typed_data
from autobahn.xbr import make_w3, EthereumKey, mnemonic_to_private_key
from autobahn.xbr._eip712_member_register import _create_eip712_member_register
from autobahn.xbr._eip712_market_create import _create_eip712_market_create
from autobahn.xbr._secmod import SecurityModuleMemory
from autobahn.wamp.cryptosign import CryptosignKey
# https://web3py.readthedocs.io/en/stable/providers.html#infura-mainnet
HAS_INFURA = 'WEB3_INFURA_PROJECT_ID' in os***REMOVED***iron and len(os***REMOVED***iron['WEB3_INFURA_PROJECT_ID']) > 0
# TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary
IS_CPY_310 = sys.version_info.minor == 10
@skipIf(not os***REMOVED***iron.get('USE_TWISTED', False), 'only for Twisted')
@skipIf(not HAS_INFURA, 'env var WEB3_INFURA_PROJECT_ID not defined')
@skipIf(not (HAS_XBR and HAS_CRYPTOSIGN), 'package autobahn[encryption,xbr] not installed')
class TestSecurityModule(TestCase):
def setUp(self):
self._gw_config = {
'type': 'infura',
'key': os***REMOVED***iron.get('WEB3_INFURA_PROJECT_ID', ''),
'network': 'mainnet',
}
self._w3 = make_w3(self._gw_config)
self._seedphrase = "avocado style uncover thrive same grace crunch want essay reduce current edge"
self._addresses = [
'[AWS-SECRET-REMOVED]57',
'[AWS-SECRET-REMOVED]F6',
'[AWS-SECRET-REMOVED]47',
'[AWS-SECRET-REMOVED]43',
'[AWS-SECRET-REMOVED]26',
]
self._keys = [
'[AWS-SECRET-REMOVED]b5610646937704aa5cfc28b15e',
'[AWS-SECRET-REMOVED]65dd337ddc3033067c1da0e735',
'[AWS-SECRET-REMOVED]ef2d8a05df7af2d95cdc127672',
'[AWS-SECRET-REMOVED]9d8e7f229e2684d5575a84214e',
'[AWS-SECRET-REMOVED]9a98e6c7237ba0788c37b473c9',
]
# create EIP712 typed data dicts from message data and schemata
verifying_contract = a2b_hex(self._addresses[0][2:])
member = a2b_hex(self._addresses[1][2:])
maker = a2b_hex(self._addresses[2][2:])
coin = a2b_hex(self._addresses[3][2:])
eula = '[AWS-SECRET-REMOVED]vmvb81'
profile = '[AWS-SECRET-REMOVED]63ceb4'
terms = '[AWS-SECRET-REMOVED]PdT3L4'
meta = '[AWS-SECRET-REMOVED]hkLyfD'
market_id = a2b_hex('5b7ee23c9353479ca49a2461c0a1deb2')
self._eip_data_objects = [
_create_eip712_member_register(chainId=1, verifyingContract=verifying_contract, member=member,
registered=666, eula=eula, profile=profile),
_create_eip712_member_register(chainId=23, verifyingContract=a2b_hex(self._addresses[0][2:]),
member=a2b_hex(self._addresses[1][2:]), registered=9999, eula=eula,
profile=profile),
_create_eip712_market_create(chainId=1, verifyingContract=verifying_contract, member=member, created=666,
marketId=market_id, coin=coin, terms=terms, meta=meta, maker=maker,
providerSecurity=10 ** 6, consumerSecurity=10 ** 6, marketFee=100),
]
self._eip_data_obj_hashes = [
'[AWS-SECRET-REMOVED]cee30ca77776344e11f612b3',
'[AWS-SECRET-REMOVED]f6b459af08d14465c5310138',
'[AWS-SECRET-REMOVED]93f5d6e475124f18cf4c595f',
]
self._eip_data_obj_signatures = [
'[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]7e65dafd1c',
'[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]5d79335e1c',
'[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]e4cb384e1c',
]
def test_ethereum_key_from_seedphrase(self):
"""
Create key from seedphrase and index.
"""
for i in range(len(self._keys)):
key = EthereumKey.from_seedphrase(self._seedphrase, i)
self.assertEqual(key.address(binary=False), self._addresses[i])
def test_ethereum_key_from_bytes(self):
"""
Create key from raw bytes.
"""
for i in range(len(self._keys)):
key_raw = a2b_hex(self._keys[i][2:])
key = EthereumKey.from_bytes(key_raw)
self.assertEqual(key.address(binary=False), self._addresses[i])
self.assertEqual(key._key.key, key_raw)
def test_ethereum_sign_typed_data_pesu_manual(self):
"""
Test using py_eth_sig_utils by doing individual steps / manually.
"""
key_raw = a2b_hex(self._keys[0][2:])
for i in range(len(self._eip_data_objects)):
data = self._eip_data_objects[i]
# encode typed data dict and return message hash
msg_hash = encode_typed_data(data)
# print('0' * 100, b2a_hex(msg_hash).decode())
self.assertEqual(msg_hash, a2b_hex(self._eip_data_obj_hashes[i]))
# sign message hash with private key
signature_vrs = ecsign(msg_hash, key_raw)
# concatenate signature components into byte string
signature = v_r_s_to_signature(*signature_vrs)
# print('1' * 100, b2a_hex(signature).decode())
# ECDSA signatures in Ethereum consist of three parameters: v, r and s.
# The signature is always 65-bytes in length.
# r = first 32 bytes of signature
# s = second 32 bytes of signature
# v = final 1 byte of signature
self.assertEqual(len(signature), 65)
self.assertEqual(signature, a2b_hex(self._eip_data_obj_signatures[i]))
def test_ethereum_sign_typed_data_pesu_highlevel(self):
"""
Test using py_eth_sig_utils with high level functions.
"""
key_raw = a2b_hex(self._keys[0][2:])
for i in range(len(self._eip_data_objects)):
data = self._eip_data_objects[i]
signature_vrs = sign_typed_data(data, key_raw)
signature = v_r_s_to_signature(*signature_vrs)
# print('2' * 100, b2a_hex(signature).decode())
self.assertEqual(len(signature), 65)
self.assertEqual(signature, a2b_hex(self._eip_data_obj_signatures[i]))
@inlineCallbacks
def test_ethereum_sign_typed_data_ab_async(self):
"""
Test using autobahn with async functions.
"""
key_raw = a2b_hex(self._keys[0][2:])
key = EthereumKey.from_bytes(key_raw)
for i in range(len(self._eip_data_objects)):
data = self._eip_data_objects[i]
signature = yield key.sign_typed_data(data)
self.assertEqual(signature, a2b_hex(self._eip_data_obj_signatures[i]))
def test_ethereum_verify_typed_data_pesu_manual(self):
"""
Test using py_eth_sig_utils by doing individual steps / manually.
"""
for i in range(len(self._eip_data_objects)):
data = self._eip_data_objects[i]
# encode typed data dict and return message hash
msg_hash = encode_typed_data(data)
signature = a2b_hex(self._eip_data_obj_signatures[i])
signature_vrs = signature_to_v_r_s(signature)
public_key = ecrecover_to_pub(msg_hash, *signature_vrs)
address_bytes = sha3(public_key)[-20:]
address = checksum_encode(address_bytes)
self.assertEqual(address, self._addresses[0])
def test_ethereum_verify_typed_data_pesu_highlevel(self):
"""
Test using py_eth_sig_utils with high level functions.
"""
for i in range(len(self._eip_data_objects)):
data = self._eip_data_objects[i]
signature = a2b_hex(self._eip_data_obj_signatures[i])
signature_vrs = signature_to_v_r_s(signature)
address = recover_typed_data(data, *signature_vrs)
self.assertEqual(address, self._addresses[0])
@inlineCallbacks
def test_ethereum_verify_typed_data_ab_async(self):
"""
Test using autobahn with async functions.
"""
key = EthereumKey.from_address(self._addresses[0])
for i in range(len(self._eip_data_objects)):
data = self._eip_data_objects[i]
signature = a2b_hex(self._eip_data_obj_signatures[i])
sig_valid = yield key.verify_typed_data(data, signature)
self.assertTrue(sig_valid)
@inlineCallbacks
def test_secmod_iterable(self):
"""
This tests:
* :meth:`SecurityModuleMemory.from_seedphrase`
* :meth:`SecurityModuleMemory.__len__`
* :meth:`SecurityModuleMemory.__iter__`
* :meth:`SecurityModuleMemory.__getitem__`
"""
sm = SecurityModuleMemory.from_seedphrase(self._seedphrase, 5, 5)
yield sm.open()
self.assertEqual(len(sm), 10)
for i, key in sm.items():
self.assertTrue(isinstance(key, EthereumKey) or isinstance(key, CryptosignKey),
'unexpected type {} returned in security module'.format(type(key)))
key_ = sm[i]
self.assertEqual(key_, key)
@inlineCallbacks
def test_secmod_create_key(self):
"""
This tests:
* :meth:`SecurityModuleMemory.create_key`
"""
sm = SecurityModuleMemory()
yield sm.open()
self.assertEqual(len(sm), 0)
for i in range(3):
idx = yield sm.create_key('ethereum')
self.assertEqual(idx, i * 2)
self.assertEqual(len(sm), i * 2 + 1)
key = sm[idx]
self.assertTrue(isinstance(key, EthereumKey))
self.assertEqual(key.security_module, sm)
self.assertEqual(key.key_no, i * 2)
self.assertEqual(key.key_type, 'ethereum')
self.assertEqual(key.can_sign, True)
idx = yield sm.create_key('cryptosign')
self.assertEqual(idx, i * 2 + 1)
self.assertEqual(len(sm), i * 2 + 2)
key = sm[idx]
self.assertTrue(isinstance(key, CryptosignKey))
self.assertEqual(key.security_module, sm)
self.assertEqual(key.key_no, i * 2 + 1)
self.assertEqual(key.key_type, 'cryptosign')
self.assertEqual(key.can_sign, True)
self.assertEqual(len(sm), 6)
@inlineCallbacks
def test_secmod_delete_key(self):
"""
This tests:
* :meth:`SecurityModuleMemory.create_key`
* :meth:`SecurityModuleMemory.delete_key`
"""
sm = SecurityModuleMemory()
yield sm.open()
self.assertEqual(len(sm), 0)
n = 10
keys = []
for i in range(n):
if random() > .5:
yield sm.create_key('ethereum')
else:
yield sm.create_key('cryptosign')
key = sm[i]
keys.append(key)
self.assertEqual(len(sm), 10)
for i in range(n):
self.assertTrue(i in sm)
yield sm.delete_key(i)
self.assertFalse(i in sm)
self.assertEqual(len(sm), n - i - 1)
@inlineCallbacks
def test_secmod_counters(self):
"""
This tests:
* :meth:`SecurityModuleMemory.__init__`
* :meth:`SecurityModuleMemory.get_counter`
* :meth:`SecurityModuleMemory.increment_counter`
"""
sm = SecurityModuleMemory()
yield sm.open()
# counters are indexed beginning with 0
counter = 0
# initially, no counters exist, and hence value must be 0
value = yield sm.get_counter(counter)
self.assertEqual(value, 0)
yield sm.get_counter(randint(0, 100))
self.assertEqual(value, 0)
# once incremented, counters exist
for counter in range(10):
for i in range(100):
value = yield sm.increment_counter(counter)
self.assertEqual(value, i + 1)
value = yield sm.get_counter(counter)
self.assertEqual(value, i + 1)
def test_cryptosign_key_from_seedphrase(self):
# seedphrase to compute keys from
seedphrase = "myth like bonus scare over problem client lizard pioneer submit female collect"
# pubkeys we expect
pubs_keys: List[str] = [
'[AWS-SECRET-REMOVED]7f3b56500b0321a908cd89ca',
'[AWS-SECRET-REMOVED]265e9698f02772867ede002f',
'[AWS-SECRET-REMOVED]0a3b7db73ef10f97ce262739',
'[AWS-SECRET-REMOVED]6648cb2a031c7451dc5ee616',
'[AWS-SECRET-REMOVED]9a529ce48a1b5f857cde0aa8',
]
# create keys from seedphrase
keys: List[CryptosignKey] = []
for i in range(5):
# BIP44 path for WAMP
# https://github.com/wamp-proto/wamp-proto/issues/401
# https://github.com/satoshilabs/slips/pull/1322
derivation_path = "m/44'/655'/0'/0/{}".format(i)
# compute private key from WAMP-Cryptosign from seedphrase and BIP44 path
key_raw = mnemonic_to_private_key(seedphrase, derivation_path)
assert type(key_raw) == bytes
assert len(key_raw) == 32
# create WAMP-Cryptosign key object from raw bytes
key = CryptosignKey.from_bytes(key_raw)
keys.append(key)
# check public keys we expect
for i in range(5):
pub_key = keys[i].public_key(binary=False)
self.assertEqual(pub_key, pubs_keys[i])
@inlineCallbacks
def test_secmod_from_seedphrase(self):
# seedphrase to compute keys from
seedphrase = "myth like bonus scare over problem client lizard pioneer submit female collect"
sm = SecurityModuleMemory.from_seedphrase(seedphrase)
yield sm.open()
self.assertEqual(len(sm), 2)
self.assertTrue(isinstance(sm[0], EthereumKey), 'unexpected type {} at index 0'.format(type(sm[0])))
self.assertTrue(isinstance(sm[1], CryptosignKey), 'unexpected type {} at index 1'.format(type(sm[1])))
yield sm.close()
sm = SecurityModuleMemory.from_seedphrase(seedphrase, num_eth_keys=5, num_cs_keys=5)
yield sm.open()
self.assertEqual(len(sm), 10)
for i in range(5):
self.assertTrue(isinstance(sm[i], EthereumKey))
for i in range(5, 10):
self.assertTrue(isinstance(sm[i], CryptosignKey))
yield sm.close()
@inlineCallbacks
def test_secmod_from_config(self):
config = pkg_resources.resource_filename('autobahn', 'xbr/test/profile/config.ini')
sm = SecurityModuleMemory.from_config(config)
yield sm.open()
self.assertEqual(len(sm), 2)
self.assertTrue(isinstance(sm[0], EthereumKey), 'unexpected type {} at index 0'.format(type(sm[0])))
self.assertTrue(isinstance(sm[1], CryptosignKey), 'unexpected type {} at index 1'.format(type(sm[1])))
key1: EthereumKey = sm[0]
key2: CryptosignKey = sm[1]
# public-key-ed25519: [AWS-SECRET-REMOVED]546f90eb5123667935d2f561
# public-adr-eth: [AWS-SECRET-REMOVED]9c
self.assertEqual(key1.address(binary=False), '[AWS-SECRET-REMOVED]9c')
self.assertEqual(key2.public_key(binary=False), '[AWS-SECRET-REMOVED]546f90eb5123667935d2f561')
yield sm.close()
@inlineCallbacks
def test_secmod_from_keyfile(self):
keyfile = pkg_resources.resource_filename('autobahn', 'xbr/test/profile/default.priv')
sm = SecurityModuleMemory.from_keyfile(keyfile)
yield sm.open()
self.assertEqual(len(sm), 2)
self.assertTrue(isinstance(sm[0], EthereumKey), 'unexpected type {} at index 0'.format(type(sm[0])))
self.assertTrue(isinstance(sm[1], CryptosignKey), 'unexpected type {} at index 1'.format(type(sm[1])))
key1: EthereumKey = sm[0]
key2: CryptosignKey = sm[1]
# public-key-ed25519: [AWS-SECRET-REMOVED]546f90eb5123667935d2f561
# public-adr-eth: [AWS-SECRET-REMOVED]9c
self.assertEqual(key1.address(binary=False), '[AWS-SECRET-REMOVED]9c')
self.assertEqual(key2.public_key(binary=False), '[AWS-SECRET-REMOVED]546f90eb5123667935d2f561')
yield sm.close()

View File

@@ -0,0 +1,57 @@
import os
import sys
from unittest import skipIf
from twisted.trial.unittest import TestCase
from autobahn.xbr import HAS_XBR
# https://web3py.readthedocs.io/en/stable/providers.html#infura-mainnet
HAS_INFURA = 'WEB3_INFURA_PROJECT_ID' in os***REMOVED***iron and len(os***REMOVED***iron['WEB3_INFURA_PROJECT_ID']) > 0
# TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary
IS_CPY_310 = sys.version_info.minor >= 10
@skipIf(not HAS_XBR, 'package autobahn[xbr] not installed')
@skipIf(not HAS_INFURA, 'env var WEB3_INFURA_PROJECT_ID not defined')
class TestWeb3(TestCase):
gw_config = {
'type': 'infura',
'key': os***REMOVED***iron.get('WEB3_INFURA_PROJECT_ID', ''),
'network': 'mainnet',
}
# "builtins.TypeError: As of 3.10, the *loop* parameter was removed from Lock() since
# it is no longer necessary"
#
# solved via websockets>=10.3, but web3==5.29.0 requires websockets<10
#
@skipIf(True, 'FIXME: web3.auto.infura was removed')
def test_connect_w3_infura_auto(self):
from web3.auto.infura import w3
self.assertTrue(w3.isConnected())
def test_connect_w3_autobahn(self):
from autobahn.xbr import make_w3
w3 = make_w3(self.gw_config)
self.assertTrue(w3.isConnected())
def test_ens_valid_names(self):
from ens.ens import ENS
for name in ['wamp-proto.eth']:
self.assertTrue(ENS.is_valid_name(name))
def test_ens_resolve_names(self):
from autobahn.xbr import make_w3
from ens.ens import ENS
w3 = make_w3(self.gw_config)
ens = ENS.from_web3(w3)
for name, adr in [
('wamp-proto.eth', '[AWS-SECRET-REMOVED]4a'),
]:
_adr = ens.address(name)
self.assertEqual(adr, _adr)