def generate_new_keypair(self, settings): """ Calls :func:`openssh_generate_new_keypair` or :func:`dropbear_generate_new_keypair` depending on what's available on the system. """ self.ssh_log.debug('generate_new_keypair()') users_ssh_dir = get_ssh_dir(self) name = 'id_ecdsa' keytype = None bits = None passphrase = '' comment = '' if 'name' in settings: name = settings['name'] if 'keytype' in settings: keytype = settings['keytype'] if 'bits' in settings: bits = settings['bits'] if 'passphrase' in settings: passphrase = settings['passphrase'] if 'comment' in settings: comment = settings['comment'] log_metadata = { 'name': name, 'keytype': keytype, 'bits': bits, 'comment': comment } self.ssh_log.info("Generating new SSH keypair", metadata=log_metadata) if which('ssh-keygen'): # Prefer OpenSSH openssh_generate_new_keypair( self, name, # Name to use when generating the keypair users_ssh_dir, # Path to save it keytype=keytype, passphrase=passphrase, bits=bits, comment=comment ) elif which('dropbearkey'): dropbear_generate_new_keypair(self, name, # Name to use when generating the keypair users_ssh_dir, # Path to save it keytype=keytype, passphrase=passphrase, bits=bits, comment=comment)
def generate_new_keypair(self, settings): """ Calls :func:`openssh_generate_new_keypair` or :func:`dropbear_generate_new_keypair` depending on what's available on the system. """ self.ssh_log.debug("generate_new_keypair()") users_ssh_dir = get_ssh_dir(self) name = "id_ecdsa" keytype = None bits = None passphrase = "" comment = "" if "name" in settings: name = settings["name"] if "keytype" in settings: keytype = settings["keytype"] if "bits" in settings: bits = settings["bits"] if "passphrase" in settings: passphrase = settings["passphrase"] if "comment" in settings: comment = settings["comment"] log_metadata = {"name": name, "keytype": keytype, "bits": bits, "comment": comment} self.ssh_log.info("Generating new SSH keypair", metadata=log_metadata) if which("ssh-keygen"): # Prefer OpenSSH openssh_generate_new_keypair( self, name, # Name to use when generating the keypair users_ssh_dir, # Path to save it keytype=keytype, passphrase=passphrase, bits=bits, comment=comment, ) elif which("dropbearkey"): dropbear_generate_new_keypair( self, name, # Name to use when generating the keypair users_ssh_dir, # Path to save it keytype=keytype, passphrase=passphrase, bits=bits, comment=comment, )
def openssh_generate_public_key(self, path, passphrase=None, settings=None): """ Generates a public key from the given private key at *path*. If a *passphrase* is provided, it will be used to generate the public key (if necessary). """ self.ssh_log.debug('openssh_generate_public_key()') ssh_keygen_path = which('ssh-keygen') pubkey_path = "%s.pub" % path command = ( "%s " # Path to ssh-keygen "-f '%s' " # Key path "-y " # Output public key to stdout "2>&1 " # Redirect stderr to stdout so we can catch failures "> '%s'" # Redirect stdout to the public key path % (ssh_keygen_path, path, pubkey_path)) import termio m = termio.Multiplex(command) def request_passphrase(*args, **kwargs): "Called if this key requires a passphrase. Ask the client to provide" message = {'terminal:sshjs_ask_passphrase': settings} self.write_message(message) def bad_passphrase(m_instance, match): "Called if the user entered a bad passphrase" settings['bad'] = True request_passphrase() if passphrase: m.expect('^Enter passphrase', "%s\n" % passphrase, optional=True, preprocess=False, timeout=5) m.expect('^load failed', bad_passphrase, optional=True, preprocess=False, timeout=5) elif settings: m.expect('^Enter passphrase', request_passphrase, optional=True, preprocess=False, timeout=5) def atexit(child, exitstatus): "Raises an SSHKeygenException if the *exitstatus* isn't 0" if exitstatus != 0: print(m.dump) raise SSHKeygenException( _("Error generating public key from private key at %s" % path)) m.spawn(exitfunc=atexit)
def open_sub_channel(self, term): """ Opens a sub-channel of communication by executing a new shell on the SSH server using OpenSSH's `Master mode <http://en.wikibooks.org/wiki/OpenSSH/Cookbook/Multiplexing>`_ capability (it spawns a new slave) and returns the resulting :class:`termio.Multiplex` instance. If a slave has already been opened for this purpose it will re-use the existing channel. """ term = int(term) global OPEN_SUBCHANNELS if term in OPEN_SUBCHANNELS and OPEN_SUBCHANNELS[term].isalive(): # Use existing sub-channel (much faster this way) return OPEN_SUBCHANNELS[term] self.ssh_log.info("Opening SSH sub-channel", metadata={'term': term}) # NOTE: When connecting a slave via ssh you can't tell it to execute a # command like you normally can (e.g. 'ssh user@host <some command>'). This # is why we're using the termio.Multiplex.expect() functionality below... session = self.ws.session session_dir = self.ws.settings['session_dir'] session_path = os.path.join(session_dir, session) if not session_path: raise SSHMultiplexingException(_( "SSH Plugin: Unable to open slave sub-channel.")) socket_path = self.loc_terms[term]['ssh_socket'] # Interesting: When using an existing socket you don't need to give it all # the same options as you used to open it but you still need to give it # *something* in place of the hostname or it will report a syntax error and # print out the help. So that's why I've put 'go_ssh_remote_cmd' below. # ...but I could have just used 'foo' :) if not socket_path: raise SSHMultiplexingException(_( "SSH Plugin: Unable to open slave sub-channel.")) users_ssh_dir = get_ssh_dir(self) ssh_config_path = os.path.join(users_ssh_dir, 'config') if not os.path.exists(ssh_config_path): # Create it (an empty one so ssh doesn't error out) with open(ssh_config_path, 'w') as f: f.write('\n') # Hopefully 'go_ssh_remote_cmd' will be a clear enough indication of # what is going on by anyone that has to review the logs... ssh = which('ssh') ssh_command = "%s -x -S'%s' -F'%s' go_ssh_remote_cmd" % ( ssh, socket_path, ssh_config_path) OPEN_SUBCHANNELS[term] = m = self.new_multiplex( ssh_command, "%s (sub)" % term) # Using huge numbers here so we don't miss much (if anything) if the user # executes something like "ps -ef". m.spawn(rows=100, cols=200) # Hopefully 100/200 lines/cols is enough # ...if it isn't, well, that's not really what this is for :) # Set the term title so it gets a proper name in the logs m.writeline(u'echo -e "\\033]0;Term %s sub-channel\\007"' % term) return m
def get_host_fingerprint(self, settings): """ Returns a the hash of the given host's public key by making a remote connection to the server (not just by looking at known_hosts). """ out_dict = {} if 'port' not in settings: port = 22 else: port = settings['port'] if 'host' not in settings: out_dict['result'] = _("Error: You must supply a 'host'.") message = {'terminal:sshjs_display_fingerprint': out_dict} self.write_message(message) else: host = settings['host'] self.ssh_log.debug("get_host_fingerprint(%s:%s)" % (host, port), metadata={ 'host': host, 'port': port }) out_dict.update({'result': 'Success', 'host': host, 'fingerprint': None}) ssh = which('ssh') command = "%s -p %s -oUserKnownHostsFile=none -F. %s" % (ssh, port, host) m = self.new_multiplex( command, 'get_host_key', logging=False) # Logging is false so we don't make tons of silly logs def grab_fingerprint(m_instance, match): out_dict['fingerprint'] = match.splitlines()[-1][:-1] m_instance.terminate() message = {'terminal:sshjs_display_fingerprint': out_dict} self.write_message(message) del m_instance def errorback(m_instance): leftovers = [a.rstrip() for a in m_instance.dump() if a.strip()] out_dict['result'] = _( "Error: Could not determine the fingerprint of %s:%s... '%s'" % (host, port, "\n".join(leftovers))) m_instance.terminate() # Don't leave stuff hanging around! message = {'terminal:sshjs_display_fingerprint': out_dict} self.write_message(message) del m_instance # "The authenticity of host 'localhost (127.0.0.1)' can't be established.\r\nECDSA key fingerprint is 83:f5:b1:f1:d3:8c:b8:fe:d3:be:e5:dd:95:a5:ba:73.\r\nAre you sure you want to continue connecting (yes/no)? " m.expect('\n.+fingerprint .+\n', grab_fingerprint, errorback=errorback, preprocess=False) m.spawn()
def get_host_fingerprint(self, settings): """ Returns a the hash of the given host's public key by making a remote connection to the server (not just by looking at known_hosts). """ out_dict = {} if 'port' not in settings: port = 22 else: port = settings['port'] if 'host' not in settings: out_dict['result'] = _("Error: You must supply a 'host'.") message = {'terminal:sshjs_display_fingerprint': out_dict} self.write_message(message) else: host = settings['host'] self.ssh_log.debug( "get_host_fingerprint(%s:%s)" % (host, port), metadata={'host': host, 'port': port}) out_dict.update({ 'result': 'Success', 'host': host, 'fingerprint': None }) ssh = which('ssh') command = "%s -p %s -oUserKnownHostsFile=none -F. %s" % (ssh, port, host) m = self.new_multiplex( command, 'get_host_key', logging=False) # Logging is false so we don't make tons of silly logs def grab_fingerprint(m_instance, match): out_dict['fingerprint'] = match.splitlines()[-1][:-1] m_instance.terminate() message = {'terminal:sshjs_display_fingerprint': out_dict} self.write_message(message) del m_instance def errorback(m_instance): leftovers = [a.rstrip() for a in m_instance.dump() if a.strip()] out_dict['result'] = _( "Error: Could not determine the fingerprint of %s:%s... '%s'" % (host, port, "\n".join(leftovers))) m_instance.terminate() # Don't leave stuff hanging around! message = {'terminal:sshjs_display_fingerprint': out_dict} self.write_message(message) del m_instance # "The authenticity of host 'localhost (127.0.0.1)' can't be established.\r\nECDSA key fingerprint is 83:f5:b1:f1:d3:8c:b8:fe:d3:be:e5:dd:95:a5:ba:73.\r\nAre you sure you want to continue connecting (yes/no)? " m.expect('\n.+fingerprint .+\n', grab_fingerprint, errorback=errorback, preprocess=False) m.spawn()
def openssh_generate_public_key(self, path, passphrase=None, settings=None): """ Generates a public key from the given private key at *path*. If a *passphrase* is provided, it will be used to generate the public key (if necessary). """ self.ssh_log.debug("openssh_generate_public_key()") ssh_keygen_path = which("ssh-keygen") pubkey_path = "%s.pub" % path command = ( "%s " # Path to ssh-keygen "-f '%s' " # Key path "-y " # Output public key to stdout "2>&1 " # Redirect stderr to stdout so we can catch failures "> '%s'" % (ssh_keygen_path, path, pubkey_path) # Redirect stdout to the public key path ) import termio m = termio.Multiplex(command) def request_passphrase(*args, **kwargs): "Called if this key requires a passphrase. Ask the client to provide" message = {"terminal:sshjs_ask_passphrase": settings} self.write_message(message) def bad_passphrase(m_instance, match): "Called if the user entered a bad passphrase" settings["bad"] = True request_passphrase() if passphrase: m.expect("^Enter passphrase", "%s\n" % passphrase, optional=True, preprocess=False, timeout=5) m.expect("^load failed", bad_passphrase, optional=True, preprocess=False, timeout=5) elif settings: m.expect("^Enter passphrase", request_passphrase, optional=True, preprocess=False, timeout=5) def atexit(child, exitstatus): "Raises an SSHKeygenException if the *exitstatus* isn't 0" if exitstatus != 0: print(m.dump) raise SSHKeygenException(_("Error generating public key from private key at %s" % path)) m.spawn(exitfunc=atexit)
def get_identities(self, *anything): """ Sends a message to the client with a list of the identities stored on the server for the current user. *anything* is just there because the client needs to send *something* along with the 'action'. """ self.ssh_log.debug("get_identities()") out_dict = {"result": "Success"} users_ssh_dir = get_ssh_dir(self) out_dict["identities"] = [] ssh_keygen_path = which("ssh-keygen") # TODO: Switch this from using ssh-keygen to determine the keytype to using the string inside the public key. try: if os.path.exists(users_ssh_dir): ssh_files = os.listdir(users_ssh_dir) for f in ssh_files: if f.endswith(".pub"): # Double-check there's also a private key... identity = f[:-4] # Will be the same name minus '.pub' if identity in ssh_files: id_path = os.path.join(users_ssh_dir, identity) pub_key_path = os.path.join(users_ssh_dir, f) public_key_contents = open(pub_key_path).read() comment = " ".join(public_key_contents.split(" ")[2:]) if public_key_contents.startswith("ecdsa"): keytype = "ECDSA" elif public_key_contents.startswith("ssh-dss"): keytype = "DSA" elif public_key_contents.startswith("ssh-rsa"): keytype = "RSA" else: keytype = "Unknown" keygen_cmd = "'%s' -vlf '%s'" % (ssh_keygen_path, id_path) retcode, key_info = shell_command(keygen_cmd) # This will just wind up as an empty string if the # version of ssh doesn't support randomart: randomart = "\n".join(key_info.splitlines()[1:]) bits = key_info.split()[0] fingerprint = key_info.split()[1] retcode, bubblebabble = shell_command("'%s' -Bf '%s'" % (ssh_keygen_path, id_path)) bubblebabble = bubblebabble.split()[1] certinfo = "" cert_path = "'%s-cert.pub'" % id_path if os.path.exists(cert_path): retcode, certinfo = shell_command("'%s' -Lf '%s'" % (ssh_keygen_path, cert_path)) certinfo = " ".join(certinfo.split(" ")[1:]) fixed_certinfo = "" for i, line in enumerate(certinfo.splitlines()): if i == 0: line = line.lstrip() fixed_certinfo += line.replace(" ", " ") fixed_certinfo += "\n" id_obj = { "name": identity, "public": public_key_contents, "keytype": keytype, "bubblebabble": bubblebabble, "fingerprint": fingerprint, "randomart": randomart, "certinfo": fixed_certinfo, "bits": bits, "comment": comment.rstrip(), } out_dict["identities"].append(id_obj) # Figure out which identities are defaults default_ids = [] default_ids_exists = False users_ssh_dir = get_ssh_dir(self) default_ids_path = os.path.join(users_ssh_dir, ".default_ids") if os.path.exists(default_ids_path): default_ids_exists = True with open(default_ids_path) as f: default_ids = f.read().splitlines() # Why not readlines()? \n # Convert any absolute paths inside default_ids to just the short names default_ids = [os.path.split(a)[1] for a in default_ids] if default_ids_exists: for i, id_obj in enumerate(out_dict["identities"]): if id_obj["name"] in default_ids: out_dict["identities"][i]["default"] = True else: out_dict["identities"][i]["default"] = False except Exception as e: error_msg = _("Error getting identities: %s" % e) self.ssh_log.error(error_msg) out_dict["result"] = error_msg message = {"terminal:sshjs_identities_list": out_dict} self.write_message(message)
def openssh_generate_new_keypair(self, name, path, keytype=None, passphrase="", bits=None, comment=""): """ Generates a new private and public key pair--stored in the user's directory using the given *name* and other optional parameters (using OpenSSH). If *keytype* is given, it must be one of "ecdsa", "rsa" or "dsa" (case insensitive). If *keytype* is "rsa" or "ecdsa", *bits* may be specified to specify the size of the key. .. note:: Defaults to generating a 521-byte ecdsa key if OpenSSH is version 5.7+. Otherwise a 2048-bit rsa key will be used. """ self.ssh_log.debug("openssh_generate_new_keypair()") openssh_version = shell_command("ssh -V")[1] ssh_major_version = int(openssh_version.split()[0].split("_")[1].split(".")[0]) key_path = os.path.join(path, name) ssh_minor_version = int(openssh_version.split()[0].split("_")[1].split(".")[1][0]) ssh_version = "%s.%s" % (ssh_major_version, ssh_minor_version) ssh_version = float(ssh_version) if not keytype: if ssh_version >= 5.7: keytype = "ecdsa" else: keytype = "rsa" else: keytype = keytype.lower() if not bits and keytype == "ecdsa": bits = 521 # Not a typo: five-hundred-twenty-one bits elif not bits and keytype == "rsa": bits = 2048 if not passphrase: # This just makes sure False and None end up as '' passphrase = "" hostname = os.uname()[1] if not comment: now = datetime.now().isoformat() comment = "Generated by Gate One on %s %s" % (hostname, now) ssh_keygen_path = which("ssh-keygen") command = ( "%s " # Path to ssh-keygen "-b %s " # bits "-t %s " # keytype "-C '%s' " # comment "-f '%s'" % (ssh_keygen_path, bits, keytype, comment, key_path) # Key path ) self.ssh_log.debug("Keygen command: %s" % command) m = self.new_multiplex(command, "gen_ssh_keypair") call_errorback = partial(errorback, self) m.expect("^Overwrite.*", overwrite, optional=True, preprocess=False, timeout=10) passphrase_handler = partial(enter_passphrase, passphrase) m.expect("^Enter passphrase", passphrase_handler, errorback=call_errorback, preprocess=False, timeout=10) m.expect("^Enter same passphrase again", passphrase_handler, errorback=call_errorback, preprocess=False, timeout=10) finalize = partial(finished, self) # The regex below captures the md5 fingerprint which tells us the # operation was successful. m.expect( "(([0-9a-f][0-9a-f]\:){15}[0-9a-f][0-9a-f])", finalize, errorback=call_errorback, preprocess=False, timeout=15, # Key generation can take a little while ) m.spawn()
def get_identities(self, *anything): """ Sends a message to the client with a list of the identities stored on the server for the current user. *anything* is just there because the client needs to send *something* along with the 'action'. """ self.ssh_log.debug('get_identities()') out_dict = {'result': 'Success'} users_ssh_dir = get_ssh_dir(self) out_dict['identities'] = [] ssh_keygen_path = which('ssh-keygen') # TODO: Switch this from using ssh-keygen to determine the keytype to using the string inside the public key. try: if os.path.exists(users_ssh_dir): ssh_files = os.listdir(users_ssh_dir) for f in ssh_files: if f.endswith('.pub'): # Double-check there's also a private key... identity = f[:-4] # Will be the same name minus '.pub' if identity in ssh_files: id_path = os.path.join(users_ssh_dir, identity) pub_key_path = os.path.join(users_ssh_dir, f) public_key_contents = open(pub_key_path).read() comment = ' '.join(public_key_contents.split(' ')[2:]) if public_key_contents.startswith('ecdsa'): keytype = 'ECDSA' elif public_key_contents.startswith('ssh-dss'): keytype = 'DSA' elif public_key_contents.startswith('ssh-rsa'): keytype = 'RSA' else: keytype = 'Unknown' keygen_cmd = "'%s' -vlf '%s'" % ( ssh_keygen_path, id_path) retcode, key_info = shell_command(keygen_cmd) # This will just wind up as an empty string if the # version of ssh doesn't support randomart: randomart = '\n'.join(key_info.splitlines()[1:]) bits = key_info.split()[0] fingerprint = key_info.split()[1] retcode, bubblebabble = shell_command( "'%s' -Bf '%s'" % (ssh_keygen_path, id_path)) bubblebabble = bubblebabble.split()[1] certinfo = '' cert_path = "'%s-cert.pub'" % id_path if os.path.exists(cert_path): retcode, certinfo = shell_command( "'%s' -Lf '%s'" % (ssh_keygen_path, cert_path)) certinfo = ' '.join(certinfo.split(' ')[1:]) fixed_certinfo = '' for i, line in enumerate(certinfo.splitlines()): if i == 0: line = line.lstrip() fixed_certinfo += line.replace(' ', ' ') fixed_certinfo += '\n' id_obj = { 'name': identity, 'public': public_key_contents, 'keytype': keytype, 'bubblebabble': bubblebabble, 'fingerprint': fingerprint, 'randomart': randomart, 'certinfo': fixed_certinfo, 'bits': bits, 'comment': comment.rstrip(), } out_dict['identities'].append(id_obj) # Figure out which identities are defaults default_ids = [] default_ids_exists = False users_ssh_dir = get_ssh_dir(self) default_ids_path = os.path.join(users_ssh_dir, '.default_ids') if os.path.exists(default_ids_path): default_ids_exists = True with open(default_ids_path) as f: default_ids = f.read().splitlines() # Why not readlines()? \n # Convert any absolute paths inside default_ids to just the short names default_ids = [os.path.split(a)[1] for a in default_ids] if default_ids_exists: for i, id_obj in enumerate(out_dict['identities']): if id_obj['name'] in default_ids: out_dict['identities'][i]['default'] = True else: out_dict['identities'][i]['default'] = False except Exception as e: error_msg = _("Error getting identities: %s" % e) self.ssh_log.error(error_msg) out_dict['result'] = error_msg message = { 'terminal:sshjs_identities_list': out_dict } self.write_message(message)
def openssh_generate_new_keypair(self, name, path, keytype=None, passphrase="", bits=None, comment=""): """ Generates a new private and public key pair--stored in the user's directory using the given *name* and other optional parameters (using OpenSSH). If *keytype* is given, it must be one of "ecdsa", "rsa" or "dsa" (case insensitive). If *keytype* is "rsa" or "ecdsa", *bits* may be specified to specify the size of the key. .. note:: Defaults to generating a 521-byte ecdsa key if OpenSSH is version 5.7+. Otherwise a 2048-bit rsa key will be used. """ self.ssh_log.debug('openssh_generate_new_keypair()') openssh_version = shell_command('ssh -V')[1] ssh_major_version = int( openssh_version.split()[0].split('_')[1].split('.')[0]) key_path = os.path.join(path, name) ssh_minor_version = int( openssh_version.split()[0].split('_')[1].split('.')[1][0]) ssh_version = "%s.%s" % (ssh_major_version, ssh_minor_version) ssh_version = float(ssh_version) if not keytype: if ssh_version >= 5.7: keytype = "ecdsa" else: keytype = "rsa" else: keytype = keytype.lower() if not bits and keytype == "ecdsa": bits = 521 # Not a typo: five-hundred-twenty-one bits elif not bits and keytype == "rsa": bits = 2048 if not passphrase: # This just makes sure False and None end up as '' passphrase = '' hostname = os.uname()[1] if not comment: now = datetime.now().isoformat() comment = "Generated by Gate One on %s %s" % (hostname, now) ssh_keygen_path = which('ssh-keygen') command = ( "%s " # Path to ssh-keygen "-b %s " # bits "-t %s " # keytype "-C '%s' " # comment "-f '%s'" # Key path % (ssh_keygen_path, bits, keytype, comment, key_path) ) self.ssh_log.debug("Keygen command: %s" % command) m = self.new_multiplex(command, "gen_ssh_keypair") call_errorback = partial(errorback, self) m.expect('^Overwrite.*', overwrite, optional=True, preprocess=False, timeout=10) passphrase_handler = partial(enter_passphrase, passphrase) m.expect('^Enter passphrase', passphrase_handler, errorback=call_errorback, preprocess=False, timeout=10) m.expect('^Enter same passphrase again', passphrase_handler, errorback=call_errorback, preprocess=False, timeout=10) finalize = partial(finished, self) # The regex below captures the md5 fingerprint which tells us the # operation was successful. m.expect( '(([0-9a-f][0-9a-f]\:){15}[0-9a-f][0-9a-f])', finalize, errorback=call_errorback, preprocess=False, timeout=15 # Key generation can take a little while ) m.spawn()