def _read_private_key(self, tag, f, password=None): lines = f.readlines() start = 0 while (start < len(lines)) and (lines[start].strip() != '-----BEGIN ' + tag + ' PRIVATE KEY-----'): start += 1 if start >= len(lines): raise SSHException('not a valid ' + tag + ' private key file') # parse any headers first headers = {} start += 1 while start < len(lines): l = lines[start].split(': ') if len(l) == 1: break headers[l[0].lower()] = l[1].strip() start += 1 # find end end = start while (lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----') and (end < len(lines)): end += 1 # if we trudged to the end of the file, just try to cope. try: data = base64.decodestring(''.join(lines[start:end])) except base64.binascii.Error, e: raise SSHException('base64 decoding error: ' + str(e))
def __init__(self, msg=None, data=None, filename=None, password=None, vals=None, file_obj=None): self.p = None self.q = None self.g = None self.y = None self.x = None if file_obj is not None: self._from_private_key(file_obj, password) return if filename is not None: self._from_private_key_file(filename, password) return if (msg is None) and (data is not None): msg = Message(data) if vals is not None: self.p, self.q, self.g, self.y = vals else: if msg is None: raise SSHException('Key object may not be empty') if msg.get_string() != 'ssh-dss': raise SSHException('Invalid key') self.p = msg.get_mpint() self.q = msg.get_mpint() self.g = msg.get_mpint() self.y = msg.get_mpint() self.size = util.bit_length(self.p)
def __init__(self, msg=None, data=None, filename=None, password=None, vals=None, file_obj=None, validate_point=True): self.verifying_key = None self.signing_key = None self.public_blob = None if file_obj is not None: self._from_private_key(file_obj, password) return if filename is not None: self._from_private_key_file(filename, password) return if (msg is None) and (data is not None): msg = Message(data) if vals is not None: self.signing_key, self.verifying_key = vals c_class = self.signing_key.curve.__class__ self.ecdsa_curve = self._ECDSA_CURVES.get_by_curve_class(c_class) else: # Must set ecdsa_curve first; subroutines called herein may need to # spit out our get_name(), which relies on this. key_type = msg.get_text() # But this also means we need to hand it a real key/curve # identifier, so strip out any cert business. (NOTE: could push # that into _ECDSACurveSet.get_by_key_format_identifier(), but it # feels more correct to do it here?) suffix = '*****@*****.**' if key_type.endswith(suffix): key_type = key_type[:-len(suffix)] self.ecdsa_curve = self._ECDSA_CURVES.get_by_key_format_identifier( key_type) key_types = self._ECDSA_CURVES.get_key_format_identifier_list() cert_types = [ '{}[email protected]'.format(x) for x in key_types ] self._check_type_and_load_cert( msg=msg, key_type=key_types, cert_type=cert_types, ) curvename = msg.get_text() if curvename != self.ecdsa_curve.nist_name: raise SSHException( "Can't handle curve of type {}".format(curvename)) pointinfo = msg.get_binary() try: numbers = ec.EllipticCurvePublicNumbers.from_encoded_point( self.ecdsa_curve.curve_class(), pointinfo) except ValueError: raise SSHException("Invalid public key") self.verifying_key = numbers.public_key(backend=default_backend())
def _read_all(self, wanted): result = self._conn.recv(wanted) while len(result) < wanted: if len(result) == 0: raise SSHException('lost ssh-agent') extra = self._conn.recv(wanted - len(result)) if len(extra) == 0: raise SSHException('lost ssh-agent') result += extra return result
def _encode_key(self): if self.x is None: raise SSHException('Not enough key information') keylist = [ 0, self.p, self.q, self.g, self.y, self.x ] try: b = BER() b.encode(keylist) except BERException: raise SSHException('Unable to create ber encoding of key') return str(b)
def _read_private_key(self, tag, f, password=None): lines = f.readlines() start = 0 beginning_of_key = '-----BEGIN ' + tag + ' PRIVATE KEY-----' while start < len(lines) and lines[start].strip() != beginning_of_key: start += 1 if start >= len(lines): raise SSHException('not a valid ' + tag + ' private key file') # parse any headers first headers = {} start += 1 while start < len(lines): l = lines[start].split(': ') if len(l) == 1: break headers[l[0].lower()] = l[1].strip() start += 1 # find end end = start ending_of_key = '-----END ' + tag + ' PRIVATE KEY-----' while end < len(lines) and lines[end].strip() != ending_of_key: end += 1 # if we trudged to the end of the file, just try to cope. try: data = decodebytes(b(''.join(lines[start:end]))) except base64.binascii.Error as e: raise SSHException('base64 decoding error: ' + str(e)) if 'proc-type' not in headers: # unencryped: done return data # encrypted keyfile: will need a password proc_type = headers['proc-type'] if proc_type != '4,ENCRYPTED': raise SSHException( 'Unknown private key structure "{}"'.format(proc_type)) try: encryption_type, saltstr = headers['dek-info'].split(',') except: raise SSHException("Can't parse DEK-info in private key file") if encryption_type not in self._CIPHER_TABLE: raise SSHException( 'Unknown private key cipher "{}"'.format(encryption_type)) # if no password was passed in, # raise an exception pointing out that we need one if password is None: raise PasswordRequiredException('Private key file is encrypted') cipher = self._CIPHER_TABLE[encryption_type]['cipher'] keysize = self._CIPHER_TABLE[encryption_type]['keysize'] mode = self._CIPHER_TABLE[encryption_type]['mode'] salt = unhexlify(b(saltstr)) key = util.generate_key_bytes(md5, salt, password, keysize) decryptor = Cipher(cipher(key), mode(salt), backend=default_backend()).decryptor() return decryptor.update(data) + decryptor.finalize()
def unpad(data): # At the moment, this is only used for unpadding private keys on disk. This # really ought to be made constant time (possibly by upstreaming this logic # into pyca/cryptography). padding_length = six.indexbytes(data, -1) if padding_length > 16: raise SSHException("Invalid key") for i in range(1, padding_length + 1): if six.indexbytes(data, -i) != (padding_length - i + 1): raise SSHException("Invalid key") return data[:-padding_length]
def _encode_key(self): if (self.p is None) or (self.q is None): raise SSHException('Not enough key info to write private key file') keylist = [ 0, self.n, self.e, self.d, self.p, self.q, self.d % (self.p - 1), self.d % (self.q - 1), util.mod_inverse(self.q, self.p) ] try: b = BER() b.encode(keylist) except BERException: raise SSHException('Unable to create ber encoding of key') return str(b)
def _parse_service_accept(self, m): service = m.get_string() if service == 'ssh-userauth': self.transport._log(DEBUG, 'userauth is OK') m = Message() m.add_byte(chr(MSG_USERAUTH_REQUEST)) m.add_string(self.username) m.add_string('ssh-connection') m.add_string(self.auth_method) if self.auth_method == 'password': m.add_boolean(False) password = self.password if isinstance(password, unicode): password = password.encode('UTF-8') m.add_string(password) elif self.auth_method == 'publickey': m.add_boolean(True) m.add_string(self.private_key.get_name()) m.add_string(str(self.private_key)) blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username) sig = self.private_key.sign_ssh_data(self.transport.rng, blob) m.add_string(str(sig)) elif self.auth_method == 'keyboard-interactive': m.add_string('') m.add_string(self.submethods) elif self.auth_method == 'none': pass else: raise SSHException('Unknown auth method "%s"' % self.auth_method) self.transport._send_message(m) else: self.transport._log(DEBUG, 'Service request "%s" accepted (?)' % service)
def exec_command(self, command): """ Execute a command on the server. If the server allows it, the channel will then be directly connected to the stdin, stdout, and stderr of the command being executed. When the command finishes executing, the channel will be closed and can't be reused. You must open a new channel if you wish to execute another command. @param command: a shell command to execute. @type command: str @raise SSHException: if the request was rejected or the channel was closed """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() m.add_byte(chr(MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) m.add_string('exec') m.add_boolean(True) m.add_string(command) self._event_pending() self.transport._send_user_message(m) self._wait_for_event()
def _decode_key(self, data): # private key file contains: # DSAPrivateKey = { version = 0, p, q, g, y, x } try: keylist = BER(data).decode() except BERException as e: raise SSHException('Unable to parse key file: ' + str(e)) if (type(keylist) is not list or len(keylist) < 6 or keylist[0] != 0): raise SSHException( 'not a valid DSA private key file (bad ber encoding)') self.p = keylist[1] self.q = keylist[2] self.g = keylist[3] self.y = keylist[4] self.x = keylist[5] self.size = util.bit_length(self.p)
def parse_next(self, ptype, m): if self.transport.server_mode and (ptype == _MSG_KEXECDH_INIT): return self._parse_kexecdh_init(m) elif not self.transport.server_mode and (ptype == _MSG_KEXECDH_REPLY): return self._parse_kexecdh_reply(m) raise SSHException( 'KexECDH asked to handle packet type {:d}'.format(ptype))
def __init__(self, sock): """ Create an SFTP client from an existing L{Channel}. The channel should already have requested the C{"sftp"} subsystem. An alternate way to create an SFTP client context is by using L{from_transport}. @param sock: an open L{Channel} using the C{"sftp"} subsystem @type sock: L{Channel} @raise SSHException: if there's an exception while negotiating sftp """ BaseSFTP.__init__(self) self.sock = sock self.ultra_debug = False self.request_number = 1 # lock for request_number self._lock = threading.Lock() self._cwd = None # request # -> SFTPFile self._expecting = weakref.WeakValueDictionary() if type(sock) is Channel: # override default logger transport = self.sock.get_transport() self.logger = util.get_logger(transport.get_log_channel() + '.sftp') self.ultra_debug = transport.get_hexdump() try: server_version = self._send_version() except EOFError, x: raise SSHException('EOF during negotiation')
def get_modulus(self, min, prefer, max): bitsizes = sorted(self.pack.keys()) if len(bitsizes) == 0: raise SSHException('no moduli available') good = -1 # find nearest bitsize >= preferred for b in bitsizes: if (b >= prefer) and (b <= max) and (b < good or good == -1): good = b # if that failed, find greatest bitsize >= min if good == -1: for b in bitsizes: if (b >= min) and (b <= max) and (b > good): good = b if good == -1: # their entire (min, max) range has no intersection with our range. # if their range is below ours, pick the smallest. otherwise pick # the largest. it'll be out of their range requirement either way, # but we'll be sending them the closest one we have. good = bitsizes[0] if min > good: good = bitsizes[-1] # now pick a random modulus of this bitsize n = _roll_random(len(self.pack[good])) return self.pack[good][n]
def _parse_kexdh_gex_request(self, m): minbits = m.get_int() preferredbits = m.get_int() maxbits = m.get_int() # smoosh the user's preferred size into our own limits if preferredbits > self.max_bits: preferredbits = self.max_bits if preferredbits < self.min_bits: preferredbits = self.min_bits # fix min/max if they're inconsistent. technically, we could just pout # and hang up, but there's no harm in giving them the benefit of the # doubt and just picking a bitsize for them. if minbits > preferredbits: minbits = preferredbits if maxbits < preferredbits: maxbits = preferredbits # now save a copy self.min_bits = minbits self.preferred_bits = preferredbits self.max_bits = maxbits # generate prime pack = self.transport._get_modulus_pack() if pack is None: raise SSHException( 'Can\'t do server-side gex with no modulus pack') self.transport._log( DEBUG, 'Picking p (%d <= %d <= %d bits)' % (minbits, preferredbits, maxbits)) self.g, self.p = pack.get_modulus(minbits, preferredbits, maxbits) m = Message() m.add_byte(chr(_MSG_KEXDH_GEX_GROUP)) m.add_mpint(self.p) m.add_mpint(self.g) self.transport._send_message(m) self.transport._expect_packet(_MSG_KEXDH_GEX_INIT)
def _decode_key(self, data): # private key file contains: # DSAPrivateKey = { version = 0, p, q, g, y, x } try: keylist = BER(data).decode() except BERException, x: raise SSHException('Unable to parse key file: ' + str(x))
def resize_pty(self, width=80, height=24, width_pixels=0, height_pixels=0): """ Resize the pseudo-terminal. This can be used to change the width and height of the terminal emulation created in a previous L{get_pty} call. @param width: new width (in characters) of the terminal screen @type width: int @param height: new height (in characters) of the terminal screen @type height: int @param width_pixels: new width (in pixels) of the terminal screen @type width_pixels: int @param height_pixels: new height (in pixels) of the terminal screen @type height_pixels: int @raise SSHException: if the request was rejected or the channel was closed """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() m.add_byte(chr(MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) m.add_string('window-change') m.add_boolean(False) m.add_int(width) m.add_int(height) m.add_int(width_pixels) m.add_int(height_pixels) self.transport._send_user_message(m)
def invoke_subsystem(self, subsystem): """ Request a subsystem on the server (for example, C{sftp}). If the server allows it, the channel will then be directly connected to the requested subsystem. When the subsystem finishes, the channel will be closed and can't be reused. @param subsystem: name of the subsystem being requested. @type subsystem: str @raise SSHException: if the request was rejected or the channel was closed """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() m.add_byte(chr(MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) m.add_string('subsystem') m.add_boolean(True) m.add_string(subsystem) self._event_pending() self.transport._send_user_message(m) self._wait_for_event()
def _parse_kexdh_gex_request_old(self, m): # same as above, but without min_bits or max_bits (used by older # clients like putty) self.preferred_bits = m.get_int() # smoosh the user's preferred size into our own limits if self.preferred_bits > self.max_bits: self.preferred_bits = self.max_bits if self.preferred_bits < self.min_bits: self.preferred_bits = self.min_bits # generate prime pack = self.transport._get_modulus_pack() if pack is None: raise SSHException( 'Can\'t do server-side gex with no modulus pack') self.transport._log( DEBUG, 'Picking p (~ {} bits)'.format(self.preferred_bits)) self.g, self.p = pack.get_modulus(self.min_bits, self.preferred_bits, self.max_bits) m = Message() m.add_byte(c_MSG_KEXDH_GEX_GROUP) m.add_mpint(self.p) m.add_mpint(self.g) self.transport._send_message(m) self.transport._expect_packet(_MSG_KEXDH_GEX_INIT) self.old_style = True
def _parse_kexdh_gex_reply(self, m): host_key = m.get_string() self.f = m.get_mpint() sig = m.get_string() if (self.f < 1) or (self.f > self.p - 1): raise SSHException('Server kex "f" is out of range') K = pow(self.f, self.x, self.p) # okay, build up the hash H of # (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K) # noqa hm = Message() hm.add(self.transport.local_version, self.transport.remote_version, self.transport.local_kex_init, self.transport.remote_kex_init, host_key) if not self.old_style: hm.add_int(self.min_bits) hm.add_int(self.preferred_bits) if not self.old_style: hm.add_int(self.max_bits) hm.add_mpint(self.p) hm.add_mpint(self.g) hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound()
def _parse_kexdh_init(self, m): # server mode self.e = m.get_mpint() if (self.e < 1) or (self.e > self.P - 1): raise SSHException('Client kex "e" is out of range') K = pow(self.e, self.x, self.P) key = self.transport.get_server_key().asbytes() # okay, build up the hash H of # (V_C || V_S || I_C || I_S || K_S || e || f || K) hm = Message() hm.add(self.transport.remote_version, self.transport.local_version, self.transport.remote_kex_init, self.transport.local_kex_init) hm.add_string(key) hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) H = sha1(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it sig = self.transport.get_server_key().sign_ssh_data(H) # send reply m = Message() m.add_byte(c_MSG_KEXDH_REPLY) m.add_string(key) m.add_mpint(self.f) m.add_string(sig) self.transport._send_message(m) self.transport._activate_outbound()
def _parse_kexgss_group(self, m): """ Parse the SSH2_MSG_KEXGSS_GROUP message (client mode). :param `Message` m: The content of the SSH2_MSG_KEXGSS_GROUP message """ self.p = m.get_mpint() self.g = m.get_mpint() # reject if p's bit length < 1024 or > 8192 bitlen = util.bit_length(self.p) if (bitlen < 1024) or (bitlen > 8192): raise SSHException( 'Server-generated gex p (don\'t ask) is out of range ' '({} bits)'.format(bitlen)) self.transport._log(DEBUG, 'Got server p ({} bits)'.format(bitlen)) # noqa self._generate_x() # now compute e = g^x mod p self.e = pow(self.g, self.x, self.p) m = Message() m.add_byte(c_MSG_KEXGSS_INIT) m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host)) m.add_mpint(self.e) self.transport._send_message(m) self.transport._expect_packet(MSG_KEXGSS_HOSTKEY, MSG_KEXGSS_CONTINUE, MSG_KEXGSS_COMPLETE, MSG_KEXGSS_ERROR)
def parse_next(self, ptype, m): if self.transport.server_mode and (ptype == _MSG_KEXDH_INIT): return self._parse_kexdh_init(m) elif not self.transport.server_mode and (ptype == _MSG_KEXDH_REPLY): return self._parse_kexdh_reply(m) msg = "KexGroup1 asked to handle packet type {:d}" raise SSHException(msg.format(ptype))
def _read_response(self, waitfor=None): while True: try: t, data = self._read_packet() except EOFError, e: raise SSHException('Server connection dropped: %s' % (str(e), )) msg = Message(data) num = msg.get_int() if num not in self._expecting: # might be response for a file that was closed before responses came back self._log(DEBUG, 'Unexpected response #%d' % (num, )) if waitfor is None: # just doing a single check break continue fileobj = self._expecting[num] del self._expecting[num] if num == waitfor: # synchronous if t == CMD_STATUS: self._convert_status(msg) return t, msg if fileobj is not type(None): fileobj._async_response(t, msg) if waitfor is None: # just doing a single check break
def invoke_shell(self): """ Request an interactive shell session on this channel. If the server allows it, the channel will then be directly connected to the stdin, stdout, and stderr of the shell. Normally you would call L{get_pty} before this, in which case the shell will operate through the pty, and the channel will be connected to the stdin and stdout of the pty. When the shell exits, the channel will be closed and can't be reused. You must open a new channel if you wish to open another shell. @raise SSHException: if the request was rejected or the channel was closed """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() m.add_byte(chr(MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) m.add_string('shell') m.add_boolean(1) self._event_pending() self.transport._send_user_message(m) self._wait_for_event()
def _check_type_and_load_cert(self, msg, key_type, cert_type): """ Perform message type-checking & optional certificate loading. This includes fast-forwarding cert ``msg`` objects past the nonce, so that the subsequent fields are the key numbers; thus the caller may expect to treat the message as key material afterwards either way. The obtained key type is returned for classes which need to know what it was (e.g. ECDSA.) """ # Normalization; most classes have a single key type and give a string, # but eg ECDSA is a 1:N mapping. key_types = key_type cert_types = cert_type if isinstance(key_type, string_types): key_types = [key_types] if isinstance(cert_types, string_types): cert_types = [cert_types] # Can't do much with no message, that should've been handled elsewhere if msg is None: raise SSHException('Key object may not be empty') # First field is always key type, in either kind of object. (make sure # we rewind before grabbing it - sometimes caller had to do their own # introspection first!) msg.rewind() type_ = msg.get_text() # Regular public key - nothing special to do besides the implicit # type check. if type_ in key_types: pass # OpenSSH-compatible certificate - store full copy as .public_blob # (so signing works correctly) and then fast-forward past the # nonce. elif type_ in cert_types: # This seems the cleanest way to 'clone' an already-being-read # message; they're *IO objects at heart and their .getvalue() # always returns the full value regardless of pointer position. self.load_certificate(Message(msg.asbytes())) # Read out nonce as it comes before the public numbers. # TODO: usefully interpret it & other non-public-number fields # (requires going back into per-type subclasses.) msg.get_string() else: err = 'Invalid key (class: {}, data type: {}' raise SSHException(err.format(self.__class__.__name__, type_))
def _decode_key(self, data): # private key file contains: # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } try: keylist = BER(data).decode() except BERException: raise SSHException('Unable to parse key file') if (type(keylist) is not list) or (len(keylist) < 4) or (keylist[0] != 0): raise SSHException( 'Not a valid RSA private key file (bad ber encoding)') self.n = keylist[1] self.e = keylist[2] self.d = keylist[3] # not really needed self.p = keylist[4] self.q = keylist[5] self.size = util.bit_length(self.n)
def missing_host_key(self, client, hostname, key): client._log( DEBUG, 'Rejecting {} host key for {}: {}'.format( key.get_name(), hostname, hexlify(key.get_fingerprint()), )) raise SSHException( 'Server {!r} not found in known_hosts'.format(hostname))
def _wait_for_event(self): self.event.wait() assert self.event.is_set() if self.event_ready: return e = self.transport.get_exception() if e is None: e = SSHException('Channel closed.') raise e
def _decode_key(self, data): try: key = serialization.load_der_private_key( data, password=None, backend=default_backend() ) except ValueError as e: raise SSHException(str(e)) assert isinstance(key, rsa.RSAPrivateKey) self.key = key