commit
0eef8c7249
5
Makefile
5
Makefile
@ -16,6 +16,11 @@ install:
|
||||
export SODIUM_INSTALL=bundled
|
||||
pip install --upgrade -e .[all,dev]
|
||||
|
||||
# upload to our internal deployment system
|
||||
upload: clean
|
||||
python setup.py bdist_wheel
|
||||
aws s3 cp dist/*.whl s3://fabric-deploy/
|
||||
|
||||
# cleanup everything
|
||||
clean:
|
||||
rm -rf ./docs/build
|
||||
|
@ -24,4 +24,4 @@
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
__version__ = u'0.17.1'
|
||||
__version__ = u'0.17.2'
|
||||
|
@ -110,7 +110,7 @@ class ApplicationRunner(object):
|
||||
self.serializers = serializers
|
||||
self.ssl = ssl
|
||||
|
||||
def run(self, make):
|
||||
def run(self, make, start_loop=True):
|
||||
"""
|
||||
Run the application component.
|
||||
|
||||
@ -118,19 +118,21 @@ class ApplicationRunner(object):
|
||||
when called with an instance of :class:`autobahn.wamp.types.ComponentConfig`.
|
||||
:type make: callable
|
||||
"""
|
||||
# 1) factory for use ApplicationSession
|
||||
def create():
|
||||
cfg = ComponentConfig(self.realm, self.extra)
|
||||
try:
|
||||
session = make(cfg)
|
||||
except Exception as e:
|
||||
self.log.error('ApplicationSession could not be instantiated: {}'.format(e))
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
loop.stop()
|
||||
raise
|
||||
else:
|
||||
return session
|
||||
if callable(make):
|
||||
def create():
|
||||
cfg = ComponentConfig(self.realm, self.extra)
|
||||
try:
|
||||
session = make(cfg)
|
||||
except Exception as e:
|
||||
self.log.error('ApplicationSession could not be instantiated: {}'.format(e))
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
loop.stop()
|
||||
raise
|
||||
else:
|
||||
return session
|
||||
else:
|
||||
create = make
|
||||
|
||||
isSecure, host, port, resource, path, params = parse_url(self.url)
|
||||
|
||||
@ -154,26 +156,32 @@ class ApplicationRunner(object):
|
||||
coro = loop.create_connection(transport_factory, host, port, ssl=ssl)
|
||||
(transport, protocol) = loop.run_until_complete(coro)
|
||||
|
||||
# start logging
|
||||
txaio.start_logging(level='info')
|
||||
if not start_loop:
|
||||
|
||||
try:
|
||||
loop.add_signal_handler(signal.SIGTERM, loop.stop)
|
||||
except NotImplementedError:
|
||||
# signals are not available on Windows
|
||||
pass
|
||||
return protocol
|
||||
|
||||
# 4) now enter the asyncio event loop
|
||||
try:
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
# wait until we send Goodbye if user hit ctrl-c
|
||||
# (done outside this except so SIGTERM gets the same handling)
|
||||
pass
|
||||
else:
|
||||
|
||||
# give Goodbye message a chance to go through, if we still
|
||||
# have an active session
|
||||
if protocol._session:
|
||||
loop.run_until_complete(protocol._session.leave())
|
||||
# start logging
|
||||
txaio.start_logging(level='info')
|
||||
|
||||
loop.close()
|
||||
try:
|
||||
loop.add_signal_handler(signal.SIGTERM, loop.stop)
|
||||
except NotImplementedError:
|
||||
# signals are not available on Windows
|
||||
pass
|
||||
|
||||
# 4) now enter the asyncio event loop
|
||||
try:
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
# wait until we send Goodbye if user hit ctrl-c
|
||||
# (done outside this except so SIGTERM gets the same handling)
|
||||
pass
|
||||
|
||||
# give Goodbye message a chance to go through, if we still
|
||||
# have an active session
|
||||
if protocol._session:
|
||||
loop.run_until_complete(protocol._session.leave())
|
||||
|
||||
loop.close()
|
||||
|
@ -187,8 +187,8 @@ class WebSocketAdapterProtocol(asyncio.Protocol):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.get_channel_id`
|
||||
"""
|
||||
# FIXME
|
||||
raise Exception("transport channel binding not implemented for asyncio")
|
||||
self.log.debug('FIXME: transport channel binding not implemented for asyncio (autobahn-python issue #729)')
|
||||
return None
|
||||
|
||||
def registerProducer(self, producer, streaming):
|
||||
raise Exception("not implemented")
|
||||
|
154
autobahn/twisted/cryptosign.py
Normal file
154
autobahn/twisted/cryptosign.py
Normal file
@ -0,0 +1,154 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) Crossbar.io Technologies 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 __future__ import absolute_import, print_function
|
||||
|
||||
from autobahn.wamp.cryptosign import HAS_CRYPTOSIGN, SigningKey
|
||||
|
||||
from twisted.internet.defer import inlineCallbacks, returnValue
|
||||
|
||||
__all__ = [
|
||||
'HAS_CRYPTOSIGN_SSHAGENT'
|
||||
]
|
||||
|
||||
if HAS_CRYPTOSIGN:
|
||||
try:
|
||||
# WAMP-cryptosign support for SSH agent is currently
|
||||
# only available on Twisted (on Python 2)
|
||||
from twisted.internet.protocol import Factory
|
||||
from twisted.internet.endpoints import UNIXClientEndpoint
|
||||
from twisted.conch.ssh.agent import SSHAgentClient
|
||||
except ImportError:
|
||||
# twisted.conch is not yet fully ported to Python 3
|
||||
HAS_CRYPTOSIGN_SSHAGENT = False
|
||||
else:
|
||||
HAS_CRYPTOSIGN_SSHAGENT = True
|
||||
__all__.append('SSHAgentSigningKey')
|
||||
|
||||
|
||||
if HAS_CRYPTOSIGN_SSHAGENT:
|
||||
|
||||
import os
|
||||
from nacl import signing
|
||||
from autobahn.wamp.cryptosign import _read_ssh_ed25519_pubkey, _unpack, _pack
|
||||
|
||||
class SSHAgentSigningKey(SigningKey):
|
||||
"""
|
||||
A WAMP-cryptosign signing key that is a proxy to a private Ed25510 key
|
||||
actually held in SSH agent.
|
||||
|
||||
An instance of this class must be create via the class method new().
|
||||
The instance only holds the public key part, whereas the private key
|
||||
counterpart is held in SSH agent.
|
||||
"""
|
||||
|
||||
def __init__(self, key, comment=None, reactor=None):
|
||||
SigningKey.__init__(self, key, comment)
|
||||
if not reactor:
|
||||
from twisted.internet import reactor
|
||||
self._reactor = reactor
|
||||
|
||||
@classmethod
|
||||
def new(cls, pubkey=None, reactor=None):
|
||||
"""
|
||||
Create a proxy for a key held in SSH agent.
|
||||
|
||||
:param pubkey: A string with a public Ed25519 key in SSH format.
|
||||
:type pubkey: unicode
|
||||
"""
|
||||
if not HAS_CRYPTOSIGN_SSHAGENT:
|
||||
raise Exception("SSH agent integration is not supported on this platform")
|
||||
|
||||
pubkey, _ = _read_ssh_ed25519_pubkey(pubkey)
|
||||
|
||||
if not reactor:
|
||||
from twisted.internet import reactor
|
||||
|
||||
if "SSH_AUTH_SOCK" not in os.environ:
|
||||
raise Exception("no ssh-agent is running!")
|
||||
|
||||
factory = Factory()
|
||||
factory.noisy = False
|
||||
factory.protocol = SSHAgentClient
|
||||
endpoint = UNIXClientEndpoint(reactor, os.environ["SSH_AUTH_SOCK"])
|
||||
d = endpoint.connect(factory)
|
||||
|
||||
@inlineCallbacks
|
||||
def on_connect(agent):
|
||||
keys = yield agent.requestIdentities()
|
||||
|
||||
# if the key is found in ssh-agent, the raw public key (32 bytes), and the
|
||||
# key comment as returned from ssh-agent
|
||||
key_data = None
|
||||
key_comment = None
|
||||
|
||||
for blob, comment in keys:
|
||||
raw = _unpack(blob)
|
||||
algo = raw[0]
|
||||
if algo == u'ssh-ed25519':
|
||||
algo, _pubkey = raw
|
||||
if _pubkey == pubkey:
|
||||
key_data = _pubkey
|
||||
key_comment = comment.decode('utf8')
|
||||
break
|
||||
|
||||
agent.transport.loseConnection()
|
||||
|
||||
if key_data:
|
||||
key = signing.VerifyKey(key_data)
|
||||
returnValue(cls(key, key_comment, reactor))
|
||||
else:
|
||||
raise Exception("Ed25519 key not held in ssh-agent")
|
||||
|
||||
return d.addCallback(on_connect)
|
||||
|
||||
def sign(self, challenge):
|
||||
if "SSH_AUTH_SOCK" not in os.environ:
|
||||
raise Exception("no ssh-agent is running!")
|
||||
|
||||
factory = Factory()
|
||||
factory.noisy = False
|
||||
factory.protocol = SSHAgentClient
|
||||
endpoint = UNIXClientEndpoint(self._reactor, os.environ["SSH_AUTH_SOCK"])
|
||||
d = endpoint.connect(factory)
|
||||
|
||||
@inlineCallbacks
|
||||
def on_connect(agent):
|
||||
# we are now connected to the locally running ssh-agent
|
||||
# that agent might be the openssh-agent, or eg on Ubuntu 14.04 by
|
||||
# default the gnome-keyring / ssh-askpass-gnome application
|
||||
blob = _pack(['ssh-ed25519', self.public_key(binary=True)])
|
||||
|
||||
# now ask the agent
|
||||
signature_blob = yield agent.signData(blob, challenge)
|
||||
algo, signature = _unpack(signature_blob)
|
||||
|
||||
agent.transport.loseConnection()
|
||||
|
||||
returnValue(signature)
|
||||
|
||||
return d.addCallback(on_connect)
|
@ -26,22 +26,17 @@
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import binascii
|
||||
import struct
|
||||
|
||||
import six
|
||||
|
||||
from txaio import create_future_success
|
||||
import txaio
|
||||
|
||||
from autobahn import util
|
||||
from autobahn.wamp.types import Challenge
|
||||
|
||||
from twisted.internet.defer import inlineCallbacks, returnValue
|
||||
|
||||
__all__ = [
|
||||
'HAS_CRYPTOSIGN',
|
||||
'HAS_CRYPTOSIGN_SSHAGENT'
|
||||
]
|
||||
|
||||
try:
|
||||
@ -49,22 +44,9 @@ try:
|
||||
from nacl import public, encoding, signing, bindings
|
||||
except ImportError:
|
||||
HAS_CRYPTOSIGN = False
|
||||
HAS_CRYPTOSIGN_SSHAGENT = False
|
||||
else:
|
||||
HAS_CRYPTOSIGN = True
|
||||
__all__.append('SigningKey')
|
||||
try:
|
||||
# WAMP-cryptosign support for SSH agent is currently
|
||||
# only available on Twisted (on Python 2)
|
||||
from twisted.internet.protocol import Factory
|
||||
from twisted.internet.endpoints import UNIXClientEndpoint
|
||||
from twisted.conch.ssh.agent import SSHAgentClient
|
||||
except ImportError:
|
||||
# twisted.conch is not yet fully ported to Python 3
|
||||
HAS_CRYPTOSIGN_SSHAGENT = False
|
||||
else:
|
||||
HAS_CRYPTOSIGN_SSHAGENT = True
|
||||
__all__.append('SSHAgentSigningKey')
|
||||
|
||||
|
||||
def _unpack(keydata):
|
||||
@ -453,9 +435,8 @@ if HAS_CRYPTOSIGN:
|
||||
# it get coerced into the concatenation of message + signature
|
||||
# not sure which order, but we don't want that. we only want
|
||||
# the signature
|
||||
return create_future_success(sig.signature)
|
||||
return txaio.create_future_success(sig.signature)
|
||||
|
||||
@inlineCallbacks
|
||||
def sign_challenge(self, session, challenge):
|
||||
"""
|
||||
Sign WAMP-cryptosign challenge.
|
||||
@ -487,16 +468,25 @@ if HAS_CRYPTOSIGN:
|
||||
data = challenge_raw
|
||||
|
||||
# a raw byte string is signed, and the signature is also a raw byte string
|
||||
signature_raw = yield self.sign(data)
|
||||
d1 = self.sign(data)
|
||||
|
||||
# convert the raw signature into a hex encode value (unicode string)
|
||||
signature_hex = binascii.b2a_hex(signature_raw).decode('ascii')
|
||||
# asyncio lacks callback chaining (and we cannot use co-routines, since we want
|
||||
# to support older Pythons), hence we need d2
|
||||
d2 = txaio.create_future()
|
||||
|
||||
# we return the concatenation of the signature and the message signed (96 bytes)
|
||||
data_hex = binascii.b2a_hex(data).decode('ascii')
|
||||
def process(signature_raw):
|
||||
# convert the raw signature into a hex encode value (unicode string)
|
||||
signature_hex = binascii.b2a_hex(signature_raw).decode('ascii')
|
||||
|
||||
# we always return a future/deferred, so handling is uniform
|
||||
returnValue(signature_hex + data_hex)
|
||||
# we return the concatenation of the signature and the message signed (96 bytes)
|
||||
data_hex = binascii.b2a_hex(data).decode('ascii')
|
||||
|
||||
sig = signature_hex + data_hex
|
||||
txaio.resolve(d2, sig)
|
||||
|
||||
txaio.add_callbacks(d1, process, None)
|
||||
|
||||
return d2
|
||||
|
||||
@classmethod
|
||||
def from_raw_key(cls, filename, comment=None):
|
||||
@ -552,103 +542,3 @@ if HAS_CRYPTOSIGN:
|
||||
key = public.PublicKey(keydata, encoder=encoding.RawEncoder)
|
||||
|
||||
return cls(key, comment)
|
||||
|
||||
|
||||
if HAS_CRYPTOSIGN_SSHAGENT:
|
||||
|
||||
class SSHAgentSigningKey(SigningKey):
|
||||
"""
|
||||
A WAMP-cryptosign signing key that is a proxy to a private Ed25510 key
|
||||
actually held in SSH agent.
|
||||
|
||||
An instance of this class must be create via the class method new().
|
||||
The instance only holds the public key part, whereas the private key
|
||||
counterpart is held in SSH agent.
|
||||
"""
|
||||
|
||||
def __init__(self, key, comment=None, reactor=None):
|
||||
SigningKey.__init__(self, key, comment)
|
||||
if not reactor:
|
||||
from twisted.internet import reactor
|
||||
self._reactor = reactor
|
||||
|
||||
@classmethod
|
||||
def new(cls, pubkey=None, reactor=None):
|
||||
"""
|
||||
Create a proxy for a key held in SSH agent.
|
||||
|
||||
:param pubkey: A string with a public Ed25519 key in SSH format.
|
||||
:type pubkey: unicode
|
||||
"""
|
||||
if not HAS_CRYPTOSIGN_SSHAGENT:
|
||||
raise Exception("SSH agent integration is not supported on this platform")
|
||||
|
||||
pubkey, _ = _read_ssh_ed25519_pubkey(pubkey)
|
||||
|
||||
if not reactor:
|
||||
from twisted.internet import reactor
|
||||
|
||||
if "SSH_AUTH_SOCK" not in os.environ:
|
||||
raise Exception("no ssh-agent is running!")
|
||||
|
||||
factory = Factory()
|
||||
factory.noisy = False
|
||||
factory.protocol = SSHAgentClient
|
||||
endpoint = UNIXClientEndpoint(reactor, os.environ["SSH_AUTH_SOCK"])
|
||||
d = endpoint.connect(factory)
|
||||
|
||||
@inlineCallbacks
|
||||
def on_connect(agent):
|
||||
keys = yield agent.requestIdentities()
|
||||
|
||||
# if the key is found in ssh-agent, the raw public key (32 bytes), and the
|
||||
# key comment as returned from ssh-agent
|
||||
key_data = None
|
||||
key_comment = None
|
||||
|
||||
for blob, comment in keys:
|
||||
raw = _unpack(blob)
|
||||
algo = raw[0]
|
||||
if algo == u'ssh-ed25519':
|
||||
algo, _pubkey = raw
|
||||
if _pubkey == pubkey:
|
||||
key_data = _pubkey
|
||||
key_comment = comment.decode('utf8')
|
||||
break
|
||||
|
||||
agent.transport.loseConnection()
|
||||
|
||||
if key_data:
|
||||
key = signing.VerifyKey(key_data)
|
||||
returnValue(cls(key, key_comment, reactor))
|
||||
else:
|
||||
raise Exception("Ed25519 key not held in ssh-agent")
|
||||
|
||||
return d.addCallback(on_connect)
|
||||
|
||||
def sign(self, challenge):
|
||||
if "SSH_AUTH_SOCK" not in os.environ:
|
||||
raise Exception("no ssh-agent is running!")
|
||||
|
||||
factory = Factory()
|
||||
factory.noisy = False
|
||||
factory.protocol = SSHAgentClient
|
||||
endpoint = UNIXClientEndpoint(self._reactor, os.environ["SSH_AUTH_SOCK"])
|
||||
d = endpoint.connect(factory)
|
||||
|
||||
@inlineCallbacks
|
||||
def on_connect(agent):
|
||||
# we are now connected to the locally running ssh-agent
|
||||
# that agent might be the openssh-agent, or eg on Ubuntu 14.04 by
|
||||
# default the gnome-keyring / ssh-askpass-gnome application
|
||||
blob = _pack(['ssh-ed25519', self.public_key(binary=True)])
|
||||
|
||||
# now ask the agent
|
||||
signature_blob = yield agent.signData(blob, challenge)
|
||||
algo, signature = _unpack(signature_blob)
|
||||
|
||||
agent.transport.loseConnection()
|
||||
|
||||
returnValue(signature)
|
||||
|
||||
return d.addCallback(on_connect)
|
||||
|
@ -5,6 +5,15 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
0.17.2
|
||||
------
|
||||
|
||||
`Published 2017-02-25 <https://pypi.python.org/pypi/autobahn/0.17.2`_
|
||||
|
||||
* new: WAMP-cryptosign elliptic curve based authentication support for asyncio
|
||||
* new: CI testing on Twisted 17.1
|
||||
* new: controller/shared attributes on ComponentConfig
|
||||
|
||||
0.17.1
|
||||
------
|
||||
|
||||
|
6
setup.py
6
setup.py
@ -130,7 +130,8 @@ extras_require_dev = [
|
||||
'pyenchant>=1.6.6', # LGPL
|
||||
'sphinxcontrib-spelling>=2.1.2', # BSD
|
||||
'sphinx_rtd_theme>=0.1.9', # BSD
|
||||
'pytest_asyncio',
|
||||
'pytest_asyncio', # Apache 2.0
|
||||
'awscli', # Apache 2.0
|
||||
]
|
||||
|
||||
# for testing by users with "python setup.py test" (not Tox, which we use)
|
||||
@ -172,7 +173,7 @@ setup(
|
||||
platforms='Any',
|
||||
install_requires=[
|
||||
'six>=1.10.0', # MIT license
|
||||
'txaio>=2.5.2', # MIT license
|
||||
'txaio>=2.6.1', # MIT license
|
||||
],
|
||||
extras_require={
|
||||
'all': extras_require_all,
|
||||
@ -217,6 +218,7 @@ setup(
|
||||
"Programming Language :: Python :: 3.3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Programming Language :: Python :: Implementation :: Jython",
|
||||
|
Loading…
Reference in New Issue
Block a user