def kozoTransport(transport, transportName, nodeName): global _transports if transport in _transports: return _transports[transport] transportFile = None paths = [os.path.dirname(os.path.abspath(__file__))] if 'KOZOTRANSPORTPATH' in os.environ: paths.extend(os.environ['KOZOTRANSPORTPATH'].split(':')) if kozoConfig('transportPath'): paths.extend(kozoConfig('transportPath').split(':')) for path in paths: if os.path.isdir(path) and os.path.isfile(path + os.sep + transport + '.py'): transportFile = path + os.sep + transport + '.py' break if transportFile is None: raise KozoError('Could not find transport:', transport, paths) importPaths = kozoConfig('importPath').split(':') if 'KOZOIMPORTPATH' in os.environ: importPaths.extend(os.environ['KOZOIMPORTPATH'].split(':')) try: transportData = _importFile( transportFile, extraPaths=filter(lambda p: p, importPaths)) except BaseException as e: raise KozoError('Error while trying to import transport', transportFile, e) if 'transportInfo' not in transportData.__dict__: raise KozoError('transportInfo not found in', transportFile) if type(transportData.transportInfo) is not type({}): raise KozoError('transportInfo is not a dictionary in', transportFile) for key in ('format', 'class', 'config'): if key not in transportData.transportInfo: raise KozoError(transportFile, '- Key not found in transportInfo:', key) if transportData.transportInfo['format'] != '1.0': raise KozoError(transport, 'has unsupported transport format', transportData.transportInfo['format']) transportClass = transportData.transportInfo['class'] transportDefaultConfig = {'type': transport} transportConfigRequired = [] for key in transportData.transportInfo['config']: if 'default' in transportData.transportInfo['config'][key]: transportDefaultConfig[key] = transportData.transportInfo[ 'config'][key]['default'] else: transportConfigRequired.append(key) transportClass._transportConfig = transportDefaultConfig transportClass._transportConfigRequired = transportConfigRequired _transports[transport] = transportClass return transportClass
def kozoRole(role, roleName, nodeName): global _roles if role in _roles: return _roles[role] roleFile = None paths = [os.path.dirname(os.path.abspath(__file__))] if 'KOZOROLEPATH' in os.environ: paths.extend(os.environ['KOZOROLEPATH'].split(':')) if kozoConfig('rolePath'): paths.extend(kozoConfig('rolePath').split(':')) for path in paths: if os.path.isdir(path) and os.path.isfile(path + os.sep + role + '.py'): roleFile = path + os.sep + role + '.py' break if roleFile is None: raise KozoError('Could not find role:', role, paths) importPaths = kozoConfig('importPath').split(':') if 'KOZOIMPORTPATH' in os.environ: importPaths.extend(os.environ['KOZOIMPORTPATH'].split(':')) try: roleData = _importFile(roleFile, extraPaths=filter(lambda p: p, importPaths)) except BaseException as e: raise KozoError('Error while trying to import role', roleFile, e) if 'roleInfo' not in roleData.__dict__: raise KozoError('roleInfo not found in', roleFile) if type(roleData.roleInfo) is not type({}): raise KozoError('roleInfo is not a dictionary in', roleFile) for key in ('format', 'class', 'config'): if key not in roleData.roleInfo: raise KozoError(roleFile, '- Key not found in roleInfo:', key) if roleData.roleInfo['format'] != '1.0': raise KozoError(role, 'has unsupported role format', roleData.roleInfo['format']) roleClass = roleData.roleInfo['class'] roleDefaultConfig = {'type': role} roleConfigRequired = [] for key in roleData.roleInfo['config']: if 'default' in roleData.roleInfo['config'][key]: roleDefaultConfig[key] = roleData.roleInfo['config'][key]['default'] else: roleConfigRequired.append(key) roleClass._roleConfig = roleDefaultConfig roleClass._roleConfigRequired = roleConfigRequired _roles[role] = roleClass return roleClass
def connect(self, otherTransport): assert self._privateKey is not None selfNode = self.getNode() selfName = selfNode.getName() remoteNode = otherTransport.getNode() remoteName = remoteNode.getName() remoteAddress = (otherTransport['address'], otherTransport['onionPort']) try: sock = socket.create_connection(remoteAddress, kozoConfig('connectionRetry')) # First, send magic knock-knock message. sock.sendall(self.MAGIC_KNOCKKNOCK + struct.pack('=H', len(selfName)) + selfName.encode('utf8')) # We expect to be asked to sign a message of length up to 32K characters. signLength = struct.unpack('=H', self._readLoop(sock.recv, struct.calcsize('=H')))[0] if signLength > self.MAX_INITIAL_SIGN_LENGTH: raise KozoError('Initial signature length too long:', signLength) # Get message to sign. initialToSign = self._readLoop(sock.recv, signLength) # Expand it. selfDate = int(time.time()) selfRandom = self._genRandom() actualToSign = self.ACTUAL_SIGN_FORMAT.format( initial=initialToSign, date=str(selfDate).encode('utf8'), server=remoteName.encode('utf8'), client=selfName.encode('utf8'), random=selfRandom ) # Sign it. Some versions of Paramiko take a random pool here, others don't. Try both. try: signedMessage = self._privateKey.sign_ssh_data(Crypto.Random.new(), actualToSign) except TypeError: signedMessage = self._privateKey.sign_ssh_data(actualToSign) signed = bytes(signedMessage) # Send signed response back. sock.sendall(struct.pack('=Qii', selfDate, len(selfRandom), len(signed)) + selfRandom + signed) # Expect acknowledgement. acknowledgement = self._readLoop(sock.recv, len(self.SIGN_OK)) if acknowledgement != self.SIGN_OK: raise KozoError('Invalid acknowledgement.') # We're all clear. return OnionChannel(selfNode, remoteNode, sock) except BaseException as e: infoTransport(self, 'Failed to connect to', remoteAddress, e, printTraceback=False)
def getUnauthenticatedSocket(self, otherTransport, addressIndex, address): return socket.create_connection(address, kozoConfig('connectionRetry'))
def _readLoop(self, read, bytes): data = _readLoop(read, bytes, kozoConfig('connectionRetry')) if data is None: raise KozoError('Could not read', bytes, 'bytes before timeout') return data
def _genRandom(self): numBytes = random.randint(*self.RANDOM_LENGTH) # We can be more lax with the range data = _readLoop(Crypto.Random.new().read, numBytes, min(self.RANDOM_DEADLINE, kozoConfig('connectionRetry'))) if data is None: raise KozoError('Could not generate', numBytes, 'of random data fast enough') return data
def accept(self): infoTransport(self, 'Waiting for a connection') assert self._serverSocket is not None selfNode = self.getNode() selfName = selfNode.getName() connection = self._serverSocket.accept()[0] try: connection.settimeout(kozoConfig('connectionRetry')) # Parse header. knockHeader = self._readLoop(connection.recv, len(self.MAGIC_KNOCKKNOCK)) if knockHeader != self.MAGIC_KNOCKKNOCK: raise KozoError('Invalid header received') nodeNameLength = struct.unpack('=H', self._readLoop(connection.recv, struct.calcsize('=H')))[0] if nodeNameLength > self._maxNodeNameLength: raise KozoError('Node length field larger than largest-named node defined in the system:', nodeNameLength) remoteName = self._readLoop(connection.recv, nodeNameLength).decode('utf8') remoteNode = kozoSystem().getNodeByName(remoteName) if remoteNode is None: raise KozoError('Unknown node name received', repr(remoteName)) remoteKey = paramiko.RSAKey(data=base64.b64decode(remoteNode.getPublicKey()[1])) # Generate message to sign. selfDate = int(time.time()) selfRandom = self._genRandom() initialToSign = self.INITIAL_SIGN_FORMAT.format( date=str(selfDate).encode('utf8'), server=selfName.encode('utf8'), client=remoteName.encode('utf8'), random=selfRandom ) # Send it. connection.sendall(struct.pack('=H', len(initialToSign)) + initialToSign) # Get and verify signed response. remoteDate, remoteRandomLength, remoteSignedLength = struct.unpack('=Qii', self._readLoop(connection.recv, struct.calcsize('=Qii'))) if abs(remoteDate - selfDate) > self.MAX_DATE_DELTA: raise KozoError('Clocks differ by', abs(remoteDate - selfDate), 'seconds; rejecting message') if remoteRandomLength < self.RANDOM_LENGTH[0]: raise KozoError('Remote random string is shorter than minimum of', self.RANDOM_LENGTH[0], 'bytes') if remoteRandomLength > self.RANDOM_LENGTH[1]: raise KozoError('Remote random string is longer than maximum of', self.RANDOM_LENGTH[1], 'bytes') remoteRandom = self._readLoop(connection.recv, remoteRandomLength) actualToSign = self.ACTUAL_SIGN_FORMAT.format( initial=initialToSign, date=str(remoteDate).encode('utf8'), server=selfName.encode('utf8'), client=remoteName.encode('utf8'), random=remoteRandom ) if len(actualToSign) > self.MAX_ACTUAL_SIGN_LENGTH: raise KozoError('Message to sign is too long:', len(actualToSign), 'bytes while max is', self.MAX_ACTUAL_SIGN_LENGTH) if remoteSignedLength > len(actualToSign) * self.SIGNED_MAX_EXPANSION_FACTOR: raise KozoError('Signed message is too long:', remoteSignedLength, 'bytes while max is', len(actualToSign) * self.SIGNED_MAX_EXPANSION_FACTOR) actualSigned = paramiko.Message(self._readLoop(connection.recv, remoteSignedLength)) if not remoteKey.verify_ssh_sig(actualToSign, actualSigned): raise KozoError('Invalid signature received') # Send acknowledgement. connection.sendall(self.SIGN_OK) # We're clear. return OnionChannel(remoteNode, selfNode, connection) except BaseException: try: connection.close() except: pass raise