Skip to content

Commit

Permalink
add SecurityModuleMemory.from_keyfile (#1582)
Browse files Browse the repository at this point in the history
* add SecurityModuleMemory.from_keyfile
  • Loading branch information
oberstet committed Jul 2, 2022
1 parent b71a19d commit c366081
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 26 deletions.
9 changes: 7 additions & 2 deletions autobahn/twisted/wamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,7 @@ def __init__(self, **kw):
"Must provide '{}' for cryptosign".format(key)
)
for key in kw.get('authextra', dict()):
if key not in ['pubkey']:
if key not in ['pubkey', 'channel_binding', 'trustroot', 'challenge']:
raise ValueError(
"Unexpected key '{}' in 'authextra'".format(key)
)
Expand All @@ -851,7 +851,12 @@ def __init__(self, **kw):
self._args = kw

def on_challenge(self, session, challenge):
return self._privkey.sign_challenge(session, challenge)
# sign the challenge with our private key.
channel_id_type = self._args['authextra'].get('channel_binding', None)
channel_id = self.transport.transport_details.channel_id.get(channel_id_type, None)
signed_challenge = self._privkey.sign_challenge(challenge, channel_id=channel_id,
channel_id_type=channel_id_type)
return signed_challenge


IAuthenticator.register(AuthCryptoSign)
Expand Down
5 changes: 4 additions & 1 deletion autobahn/wamp/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,10 @@ def authextra(self):
return self._args.get('authextra', dict())

def on_challenge(self, session, challenge):
return self._privkey.sign_challenge(session, challenge, channel_id_type=self._channel_binding)
channel_id = session._transport.transport_details.channel_id.get(self._channel_binding, None)
return self._privkey.sign_challenge(challenge,
channel_id=channel_id,
channel_id_type=self._channel_binding)

def on_welcome(self, msg, authextra):
return None
Expand Down
19 changes: 6 additions & 13 deletions autobahn/wamp/cryptosign.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import txaio

from autobahn import util
from autobahn.wamp.interfaces import ISecurityModule, ICryptosignKey, ISession
from autobahn.wamp.interfaces import ISecurityModule, ICryptosignKey
from autobahn.wamp.types import Challenge
from autobahn.wamp.message import _URI_PAT_REALM_NAME_ETH

Expand Down Expand Up @@ -361,7 +361,7 @@ def _verify_signify_ed25519_signature(pubkey_file, signature_file, message):

if HAS_CRYPTOSIGN:

def format_challenge(challenge: Challenge, channel_id_raw: Optional[bytes], channel_id_type: Optional[str]) -> bytes:
def _format_challenge(challenge: Challenge, channel_id_raw: Optional[bytes], channel_id_type: Optional[str]) -> bytes:
"""
Format the challenge based on provided parameters
Expand Down Expand Up @@ -405,7 +405,7 @@ def format_challenge(challenge: Challenge, channel_id_raw: Optional[bytes], chan

return data

def sign_challenge(data: bytes, signer_func: Callable) -> bytes:
def _sign_challenge(data: bytes, signer_func: Callable) -> bytes:
"""
Sign the provided data using the provided signer.
Expand Down Expand Up @@ -509,24 +509,17 @@ def sign(self, data: bytes) -> bytes:
return txaio.create_future_success(sig.signature)

@util.public
def sign_challenge(self, session: ISession, challenge: Challenge,
def sign_challenge(self, challenge: Challenge, channel_id: Optional[bytes] = None,
channel_id_type: Optional[str] = None) -> bytes:
"""
Implements :meth:`autobahn.wamp.interfaces.ICryptosignKey.sign_challenge`.
"""
assert challenge.method in ['cryptosign', 'cryptosign-proxy'], \
'unexpected cryptosign challenge with method "{}"'.format(challenge.method)

# get the TLS channel ID of the underlying TLS connection
if channel_id_type and channel_id_type in session._transport.transport_details.channel_id:
channel_id = session._transport.transport_details.channel_id.get(channel_id_type, None)
else:
channel_id_type = None
channel_id = None

data = format_challenge(challenge, channel_id, channel_id_type)
data = _format_challenge(challenge, channel_id, channel_id_type)

return sign_challenge(data, self.sign)
return _sign_challenge(data, self.sign)

@util.public
def public_key(self, binary: bool = False) -> Union[str, bytes]:
Expand Down
6 changes: 2 additions & 4 deletions autobahn/wamp/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,8 +523,7 @@ def define(self, exception: Exception, error: Optional[str] = None):

@public
@abc.abstractmethod
def call(self, procedure: str, *args: Optional[List[Any]], **kwargs: Optional[Dict[str, Any]]) -> \
Union[Any, CallResult]:
def call(self, procedure: str, *args, **kwargs) -> Union[Any, CallResult]:
"""
Call a remote procedure.
Expand Down Expand Up @@ -605,8 +604,7 @@ def register(self, endpoint: Union[Callable, Any], procedure: Optional[str] = No

@public
@abc.abstractmethod
def publish(self, topic: str, *args: Optional[List[Any]], **kwargs: Optional[Dict[str, Any]]) -> \
Optional[Publication]:
def publish(self, topic: str, *args, **kwargs) -> Optional[Publication]:
"""
Publish an event to a topic.
Expand Down
8 changes: 6 additions & 2 deletions autobahn/wamp/test/test_wamp_cryptosign.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ def test_vectors(self):
challenge = types.Challenge("cryptosign", dict(challenge=testvec['challenge']))

# ok, now sign the challenge
f_signed = private_key.sign_challenge(session, challenge, channel_id_type=channel_id_type)
f_signed = private_key.sign_challenge(challenge,
channel_id=channel_id,
channel_id_type=channel_id_type)

def success(signed):
# the signature returned is a Hex encoded string
Expand Down Expand Up @@ -176,7 +178,9 @@ def test_valid(self):
session._transport.transport_details = self.transport_details

challenge = types.Challenge("cryptosign", dict(challenge="ff" * 32))
f_signed = self.key.sign_challenge(session, challenge, channel_id_type='tls-unique')
f_signed = self.key.sign_challenge(challenge,
channel_id=self.transport_details.channel_id['tls-unique'],
channel_id_type='tls-unique')

def success(signed):
self.assertEqual(
Expand Down
9 changes: 7 additions & 2 deletions autobahn/xbr/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@

class Client(ApplicationSession):

# when running over TLS, require TLS channel binding
CHANNEL_BINDING = 'tls-unique'

def __init__(self, config=None):
ApplicationSession.__init__(self, config)

Expand Down Expand Up @@ -144,7 +147,7 @@ def onConnect(self):
'pubkey': self._key.public_key(),
'trustroot': None,
'challenge': None,
'channel_binding': 'tls-unique'
'channel_binding': self.CHANNEL_BINDING,
}
self.log.info('Client connected, now joining realm "{realm}" with WAMP-cryptosign authentication ..',
realm=hlid(self.config.realm))
Expand All @@ -156,7 +159,9 @@ def onConnect(self):

def onChallenge(self, challenge):
if challenge.method == 'cryptosign':
signed_challenge = self._key.sign_challenge(self, challenge)
signed_challenge = self._key.sign_challenge(challenge,
channel_id=self.transport.transport_details.channel_id.get(self.CHANNEL_BINDING, None),
channel_id_type=self.CHANNEL_BINDING)
return signed_challenge
else:
raise RuntimeError('unable to process authentication method {}'.format(challenge.method))
Expand Down
35 changes: 35 additions & 0 deletions autobahn/xbr/_secmod.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,5 +476,40 @@ def from_config(cls, config: str, profile: str = 'default') -> 'SecurityModuleMe
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: 6b08b6e186bd2a3b9b2f36e6ece3f8031fe788ab3dc4a1cfd3a489ea387c496b
# private-key-ed25519: 20e8c05d0ede9506462bb049c4843032b18e8e75b314583d0c8d8a4942f9be40
data = _parse_user_key_file(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)
18 changes: 17 additions & 1 deletion autobahn/xbr/_userkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,24 @@ def _parse_user_key_file(key_path: str, private: bool = True) -> OrderedDict:
if os.path.exists(key_path) and not os.path.isfile(key_path):
raise Exception("Key file '{}' exists, but isn't a file".format(key_path))

allowed_tags = ['public-key-ed25519', 'public-adr-eth', 'user-id', 'created-at', 'creator']
allowed_tags = [
# common tags
'public-key-ed25519',
'public-adr-eth',
'created-at',
'creator',

# user profile
'user-id',

# node profile
'machine-id',
'node-authid',
'node-cluster-ip',
]

if private:
# private key file tags
allowed_tags.extend(['private-key-ed25519', 'private-key-eth'])

tags = OrderedDict() # type: ignore
Expand Down
21 changes: 21 additions & 0 deletions autobahn/xbr/test/test_xbr_secmod.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,24 @@ def test_secmod_from_config(self):
self.assertEqual(key2.public_key(binary=False), '15cfa4acef5cc312e0b9ba77634849d0a8c6222a546f90eb5123667935d2f561')

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: 15cfa4acef5cc312e0b9ba77634849d0a8c6222a546f90eb5123667935d2f561
# public-adr-eth: 0xe59C7418403CF1D973485B36660728a5f4A8fF9c
self.assertEqual(key1.address(binary=False), '0xe59C7418403CF1D973485B36660728a5f4A8fF9c')
self.assertEqual(key2.public_key(binary=False), '15cfa4acef5cc312e0b9ba77634849d0a8c6222a546f90eb5123667935d2f561')

yield sm.close()
2 changes: 1 addition & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Changelog
22.6.1
------

* new: add SecurityModuleMemory.from_config
* new: add SecurityModuleMemory.from_config and SecurityModuleMemory.from_keyfile
* new: moved UserKey from crossbar to autobahn
* fix: more WAMP-Cryptosign unit tests
* new: experimental WAMP API catalog support
Expand Down

0 comments on commit c366081

Please sign in to comment.