12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258 |
- # -*- test-case-name: twisted.conch.test.test_transport -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- """
- The lowest level SSH protocol. This handles the key negotiation, the
- encryption and the compression. The transport layer is described in
- RFC 4253.
- Maintainer: Paul Swartz
- """
- from __future__ import annotations
- import binascii
- import hmac
- import struct
- import types
- import zlib
- from hashlib import md5, sha1, sha256, sha384, sha512
- from typing import Any, Callable, Dict, Tuple, Union
- from cryptography.exceptions import UnsupportedAlgorithm
- from cryptography.hazmat.backends import default_backend
- from cryptography.hazmat.primitives import serialization
- from cryptography.hazmat.primitives.asymmetric import dh, ec, x25519
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
- from typing_extensions import Literal
- from twisted import __version__ as twisted_version
- from twisted.conch.ssh import _kex, address, keys
- from twisted.conch.ssh.common import MP, NS, ffs, getMP, getNS
- from twisted.internet import defer, protocol
- from twisted.logger import Logger
- from twisted.python import randbytes
- from twisted.python.compat import iterbytes, networkString
- # This import is needed if SHA256 hashing is used.
- # from twisted.python.compat import nativeString
- def _mpFromBytes(data):
- """Make an SSH multiple-precision integer from big-endian L{bytes}.
- Used in ECDH key exchange.
- @type data: L{bytes}
- @param data: The input data, interpreted as a big-endian octet string.
- @rtype: L{bytes}
- @return: The given data encoded as an SSH multiple-precision integer.
- """
- return MP(int.from_bytes(data, "big"))
- # from https://github.com/python/typeshed/blob/703ed36d5a5c9505c903ea2182e6eed679d9bddb/stdlib/hmac.pyi#L9-L10
- _Hash = Any
- _DigestMod = Union[str, Callable[[], _Hash], types.ModuleType]
- class _MACParams(Tuple[_DigestMod, bytes, bytes, int]):
- """
- L{_MACParams} represents the parameters necessary to compute SSH MAC
- (Message Authenticate Codes).
- L{_MACParams} is a L{tuple} subclass to maintain compatibility with older
- versions of the code. The elements of a L{_MACParams} are::
- 0. The digest object used for the MAC
- 1. The inner pad ("ipad") string
- 2. The outer pad ("opad") string
- 3. The size of the digest produced by the digest object
- L{_MACParams} is also an object lesson in why tuples are a bad type for
- public APIs.
- @ivar key: The HMAC key which will be used.
- """
- key: bytes
- class SSHCiphers:
- """
- SSHCiphers represents all the encryption operations that need to occur
- to encrypt and authenticate the SSH connection.
- @cvar cipherMap: A dictionary mapping SSH encryption names to 3-tuples of
- (<cryptography.hazmat.primitives.interfaces.CipherAlgorithm>,
- <block size>, <cryptography.hazmat.primitives.interfaces.Mode>)
- @cvar macMap: A dictionary mapping SSH MAC names to hash modules.
- @ivar outCipType: the string type of the outgoing cipher.
- @ivar inCipType: the string type of the incoming cipher.
- @ivar outMACType: the string type of the incoming MAC.
- @ivar inMACType: the string type of the incoming MAC.
- @ivar encBlockSize: the block size of the outgoing cipher.
- @ivar decBlockSize: the block size of the incoming cipher.
- @ivar verifyDigestSize: the size of the incoming MAC.
- @ivar outMAC: a tuple of (<hash module>, <inner key>, <outer key>,
- <digest size>) representing the outgoing MAC.
- @ivar inMAc: see outMAC, but for the incoming MAC.
- """
- cipherMap = {
- b"3des-cbc": (algorithms.TripleDES, 24, modes.CBC),
- b"aes256-cbc": (algorithms.AES, 32, modes.CBC),
- b"aes192-cbc": (algorithms.AES, 24, modes.CBC),
- b"aes128-cbc": (algorithms.AES, 16, modes.CBC),
- b"aes128-ctr": (algorithms.AES, 16, modes.CTR),
- b"aes192-ctr": (algorithms.AES, 24, modes.CTR),
- b"aes256-ctr": (algorithms.AES, 32, modes.CTR),
- b"3des-ctr": (algorithms.TripleDES, 24, modes.CTR),
- b"none": (None, 0, modes.CBC),
- }
- macMap = {
- b"hmac-sha2-512": sha512,
- b"hmac-sha2-384": sha384,
- b"hmac-sha2-256": sha256,
- b"hmac-sha1": sha1,
- b"hmac-md5": md5,
- b"none": None,
- }
- def __init__(self, outCip, inCip, outMac, inMac):
- self.outCipType = outCip
- self.inCipType = inCip
- self.outMACType = outMac
- self.inMACType = inMac
- self.encBlockSize = 0
- self.decBlockSize = 0
- self.verifyDigestSize = 0
- self.outMAC = (None, b"", b"", 0)
- self.inMAC = (None, b"", b"", 0)
- def setKeys(self, outIV, outKey, inIV, inKey, outInteg, inInteg):
- """
- Set up the ciphers and hashes using the given keys,
- @param outIV: the outgoing initialization vector
- @param outKey: the outgoing encryption key
- @param inIV: the incoming initialization vector
- @param inKey: the incoming encryption key
- @param outInteg: the outgoing integrity key
- @param inInteg: the incoming integrity key.
- """
- o = self._getCipher(self.outCipType, outIV, outKey)
- self.encryptor = o.encryptor()
- self.encBlockSize = o.algorithm.block_size // 8
- o = self._getCipher(self.inCipType, inIV, inKey)
- self.decryptor = o.decryptor()
- self.decBlockSize = o.algorithm.block_size // 8
- self.outMAC = self._getMAC(self.outMACType, outInteg)
- self.inMAC = self._getMAC(self.inMACType, inInteg)
- if self.inMAC:
- self.verifyDigestSize = self.inMAC[3]
- def _getCipher(self, cip, iv, key):
- """
- Creates an initialized cipher object.
- @param cip: the name of the cipher, maps into cipherMap
- @param iv: the initialzation vector
- @param key: the encryption key
- @return: the cipher object.
- """
- algorithmClass, keySize, modeClass = self.cipherMap[cip]
- if algorithmClass is None:
- return _DummyCipher()
- return Cipher(
- algorithmClass(key[:keySize]),
- modeClass(iv[: algorithmClass.block_size // 8]),
- backend=default_backend(),
- )
- def _getMAC(
- self, mac: bytes, key: bytes
- ) -> tuple[None, Literal[b""], Literal[b""], Literal[0]] | _MACParams:
- """
- Gets a 4-tuple representing the message authentication code.
- (<hash module>, <inner hash value>, <outer hash value>,
- <digest size>)
- @type mac: L{bytes}
- @param mac: a key mapping into macMap
- @type key: L{bytes}
- @param key: the MAC key.
- @rtype: L{bytes}
- @return: The MAC components.
- """
- mod = self.macMap[mac]
- if not mod:
- return (None, b"", b"", 0)
- # With stdlib we can only get attributes fron an instantiated object.
- hashObject = mod()
- digestSize = hashObject.digest_size
- blockSize = hashObject.block_size
- # Truncation here appears to contravene RFC 2104, section 2. However,
- # implementing the hashing behavior prescribed by the RFC breaks
- # interoperability with OpenSSH (at least version 5.5p1).
- key = key[:digestSize] + (b"\x00" * (blockSize - digestSize))
- i = key.translate(hmac.trans_36)
- o = key.translate(hmac.trans_5C)
- result = _MACParams((mod, i, o, digestSize))
- result.key = key
- return result
- def encrypt(self, blocks):
- """
- Encrypt some data.
- @type blocks: L{bytes}
- @param blocks: The data to encrypt.
- @rtype: L{bytes}
- @return: The encrypted data.
- """
- return self.encryptor.update(blocks)
- def decrypt(self, blocks):
- """
- Decrypt some data.
- @type blocks: L{bytes}
- @param blocks: The data to decrypt.
- @rtype: L{bytes}
- @return: The decrypted data.
- """
- return self.decryptor.update(blocks)
- def makeMAC(self, seqid, data):
- """
- Create a message authentication code (MAC) for the given packet using
- the outgoing MAC values.
- @type seqid: L{int}
- @param seqid: The sequence ID of the outgoing packet.
- @type data: L{bytes}
- @param data: The data to create a MAC for.
- @rtype: L{str}
- @return: The serialized MAC.
- """
- if not self.outMAC[0]:
- return b""
- data = struct.pack(">L", seqid) + data
- return hmac.HMAC(self.outMAC.key, data, self.outMAC[0]).digest()
- def verify(self, seqid, data, mac):
- """
- Verify an incoming MAC using the incoming MAC values.
- @type seqid: L{int}
- @param seqid: The sequence ID of the incoming packet.
- @type data: L{bytes}
- @param data: The packet data to verify.
- @type mac: L{bytes}
- @param mac: The MAC sent with the packet.
- @rtype: L{bool}
- @return: C{True} if the MAC is valid.
- """
- if not self.inMAC[0]:
- return mac == b""
- data = struct.pack(">L", seqid) + data
- outer = hmac.HMAC(self.inMAC.key, data, self.inMAC[0]).digest()
- return hmac.compare_digest(mac, outer)
- def _getSupportedCiphers():
- """
- Build a list of ciphers that are supported by the backend in use.
- @return: a list of supported ciphers.
- @rtype: L{list} of L{str}
- """
- supportedCiphers = []
- cs = [
- b"aes256-ctr",
- b"aes256-cbc",
- b"aes192-ctr",
- b"aes192-cbc",
- b"aes128-ctr",
- b"aes128-cbc",
- b"3des-ctr",
- b"3des-cbc",
- ]
- for cipher in cs:
- algorithmClass, keySize, modeClass = SSHCiphers.cipherMap[cipher]
- try:
- Cipher(
- algorithmClass(b" " * keySize),
- modeClass(b" " * (algorithmClass.block_size // 8)),
- backend=default_backend(),
- ).encryptor()
- except UnsupportedAlgorithm:
- pass
- else:
- supportedCiphers.append(cipher)
- return supportedCiphers
- class SSHTransportBase(protocol.Protocol):
- """
- Protocol supporting basic SSH functionality: sending/receiving packets
- and message dispatch. To connect to or run a server, you must use
- SSHClientTransport or SSHServerTransport.
- @ivar protocolVersion: A string representing the version of the SSH
- protocol we support. Currently defaults to '2.0'.
- @ivar version: A string representing the version of the server or client.
- Currently defaults to 'Twisted'.
- @ivar comment: An optional string giving more information about the
- server or client.
- @ivar supportedCiphers: A list of strings representing the encryption
- algorithms supported, in order from most-preferred to least.
- @ivar supportedMACs: A list of strings representing the message
- authentication codes (hashes) supported, in order from most-preferred
- to least. Both this and supportedCiphers can include 'none' to use
- no encryption or authentication, but that must be done manually,
- @ivar supportedKeyExchanges: A list of strings representing the
- key exchanges supported, in order from most-preferred to least.
- @ivar supportedPublicKeys: A list of strings representing the
- public key algorithms supported, in order from most-preferred to
- least.
- @ivar supportedCompressions: A list of strings representing compression
- types supported, from most-preferred to least.
- @ivar supportedLanguages: A list of strings representing languages
- supported, from most-preferred to least.
- @ivar supportedVersions: A container of strings representing supported ssh
- protocol version numbers.
- @ivar isClient: A boolean indicating whether this is a client or server.
- @ivar gotVersion: A boolean indicating whether we have received the
- version string from the other side.
- @ivar buf: Data we've received but hasn't been parsed into a packet.
- @ivar outgoingPacketSequence: the sequence number of the next packet we
- will send.
- @ivar incomingPacketSequence: the sequence number of the next packet we
- are expecting from the other side.
- @ivar outgoingCompression: an object supporting the .compress(str) and
- .flush() methods, or None if there is no outgoing compression. Used to
- compress outgoing data.
- @ivar outgoingCompressionType: A string representing the outgoing
- compression type.
- @ivar incomingCompression: an object supporting the .decompress(str)
- method, or None if there is no incoming compression. Used to
- decompress incoming data.
- @ivar incomingCompressionType: A string representing the incoming
- compression type.
- @ivar ourVersionString: the version string that we sent to the other side.
- Used in the key exchange.
- @ivar otherVersionString: the version string sent by the other side. Used
- in the key exchange.
- @ivar ourKexInitPayload: the MSG_KEXINIT payload we sent. Used in the key
- exchange.
- @ivar otherKexInitPayload: the MSG_KEXINIT payload we received. Used in
- the key exchange
- @ivar sessionID: a string that is unique to this SSH session. Created as
- part of the key exchange, sessionID is used to generate the various
- encryption and authentication keys.
- @ivar service: an SSHService instance, or None. If it's set to an object,
- it's the currently running service.
- @ivar kexAlg: the agreed-upon key exchange algorithm.
- @ivar keyAlg: the agreed-upon public key type for the key exchange.
- @ivar currentEncryptions: an SSHCiphers instance. It represents the
- current encryption and authentication options for the transport.
- @ivar nextEncryptions: an SSHCiphers instance. Held here until the
- MSG_NEWKEYS messages are exchanged, when nextEncryptions is
- transitioned to currentEncryptions.
- @ivar first: the first bytes of the next packet. In order to avoid
- decrypting data twice, the first bytes are decrypted and stored until
- the whole packet is available.
- @ivar _keyExchangeState: The current protocol state with respect to key
- exchange. This is either C{_KEY_EXCHANGE_NONE} if no key exchange is
- in progress (and returns to this value after any key exchange
- completqes), C{_KEY_EXCHANGE_REQUESTED} if this side of the connection
- initiated a key exchange, and C{_KEY_EXCHANGE_PROGRESSING} if the other
- side of the connection initiated a key exchange. C{_KEY_EXCHANGE_NONE}
- is the initial value (however SSH connections begin with key exchange,
- so it will quickly change to another state).
- @ivar _blockedByKeyExchange: Whenever C{_keyExchangeState} is not
- C{_KEY_EXCHANGE_NONE}, this is a C{list} of pending messages which were
- passed to L{sendPacket} but could not be sent because it is not legal
- to send them while a key exchange is in progress. When the key
- exchange completes, another attempt is made to send these messages.
- @ivar _peerSupportsExtensions: a boolean indicating whether the other side
- of the connection supports RFC 8308 extension negotiation.
- @ivar peerExtensions: a dict of extensions supported by the other side of
- the connection.
- """
- _log = Logger()
- protocolVersion = b"2.0"
- version = b"Twisted_" + twisted_version.encode("ascii")
- comment = b""
- ourVersionString = (
- b"SSH-" + protocolVersion + b"-" + version + b" " + comment
- ).strip()
- # L{None} is supported as cipher and hmac. For security they are disabled
- # by default. To enable them, subclass this class and add it, or do:
- # SSHTransportBase.supportedCiphers.append('none')
- # List ordered by preference.
- supportedCiphers = _getSupportedCiphers()
- supportedMACs = [
- b"hmac-sha2-512",
- b"hmac-sha2-384",
- b"hmac-sha2-256",
- b"hmac-sha1",
- b"hmac-md5",
- # `none`,
- ]
- supportedKeyExchanges = _kex.getSupportedKeyExchanges()
- supportedPublicKeys = []
- # Add the supported EC keys, and change the name from ecdh* to ecdsa*
- for eckey in supportedKeyExchanges:
- if eckey.find(b"ecdh") != -1:
- supportedPublicKeys += [eckey.replace(b"ecdh", b"ecdsa")]
- supportedPublicKeys += [b"rsa-sha2-512", b"rsa-sha2-256", b"ssh-rsa", b"ssh-dss"]
- if default_backend().ed25519_supported():
- supportedPublicKeys.append(b"ssh-ed25519")
- supportedCompressions = [b"none", b"zlib"]
- supportedLanguages = ()
- supportedVersions = (b"1.99", b"2.0")
- isClient = False
- gotVersion = False
- buf = b""
- outgoingPacketSequence = 0
- incomingPacketSequence = 0
- outgoingCompression = None
- incomingCompression = None
- sessionID = None
- service = None
- # There is no key exchange activity in progress.
- _KEY_EXCHANGE_NONE = "_KEY_EXCHANGE_NONE"
- # Key exchange is in progress and we started it.
- _KEY_EXCHANGE_REQUESTED = "_KEY_EXCHANGE_REQUESTED"
- # Key exchange is in progress and both sides have sent KEXINIT messages.
- _KEY_EXCHANGE_PROGRESSING = "_KEY_EXCHANGE_PROGRESSING"
- # There is a fourth conceptual state not represented here: KEXINIT received
- # but not sent. Since we always send a KEXINIT as soon as we get it, we
- # can't ever be in that state.
- # The current key exchange state.
- _keyExchangeState = _KEY_EXCHANGE_NONE
- _blockedByKeyExchange = None
- # Added to key exchange algorithms by a client to indicate support for
- # extension negotiation.
- _EXT_INFO_C = b"ext-info-c"
- # Added to key exchange algorithms by a server to indicate support for
- # extension negotiation.
- _EXT_INFO_S = b"ext-info-s"
- _peerSupportsExtensions = False
- peerExtensions: Dict[bytes, bytes] = {}
- def connectionLost(self, reason):
- """
- When the underlying connection is closed, stop the running service (if
- any), and log out the avatar (if any).
- @type reason: L{twisted.python.failure.Failure}
- @param reason: The cause of the connection being closed.
- """
- if self.service:
- self.service.serviceStopped()
- if hasattr(self, "avatar"):
- self.logoutFunction()
- self._log.info("connection lost")
- def connectionMade(self):
- """
- Called when the connection is made to the other side. We sent our
- version and the MSG_KEXINIT packet.
- """
- self.transport.write(self.ourVersionString + b"\r\n")
- self.currentEncryptions = SSHCiphers(b"none", b"none", b"none", b"none")
- self.currentEncryptions.setKeys(b"", b"", b"", b"", b"", b"")
- self.sendKexInit()
- def sendKexInit(self):
- """
- Send a I{KEXINIT} message to initiate key exchange or to respond to a
- key exchange initiated by the peer.
- @raise RuntimeError: If a key exchange has already been started and it
- is not appropriate to send a I{KEXINIT} message at this time.
- @return: L{None}
- """
- if self._keyExchangeState != self._KEY_EXCHANGE_NONE:
- raise RuntimeError(
- "Cannot send KEXINIT while key exchange state is %r"
- % (self._keyExchangeState,)
- )
- supportedKeyExchanges = list(self.supportedKeyExchanges)
- # Advertise extension negotiation (RFC 8308, section 2.1). At
- # present, the Conch client processes the "server-sig-algs"
- # extension (section 3.1), and the Conch server sends that but
- # ignores any extensions sent by the client, so strictly speaking at
- # the moment we only need to send this in the client case; however,
- # there's nothing to forbid the server from sending it as well, and
- # doing so makes things easier if it needs to process extensions
- # sent by clients in future.
- supportedKeyExchanges.append(
- self._EXT_INFO_C if self.isClient else self._EXT_INFO_S
- )
- self.ourKexInitPayload = b"".join(
- [
- bytes((MSG_KEXINIT,)),
- randbytes.secureRandom(16),
- NS(b",".join(supportedKeyExchanges)),
- NS(b",".join(self.supportedPublicKeys)),
- NS(b",".join(self.supportedCiphers)),
- NS(b",".join(self.supportedCiphers)),
- NS(b",".join(self.supportedMACs)),
- NS(b",".join(self.supportedMACs)),
- NS(b",".join(self.supportedCompressions)),
- NS(b",".join(self.supportedCompressions)),
- NS(b",".join(self.supportedLanguages)),
- NS(b",".join(self.supportedLanguages)),
- b"\000\000\000\000\000",
- ]
- )
- self.sendPacket(MSG_KEXINIT, self.ourKexInitPayload[1:])
- self._keyExchangeState = self._KEY_EXCHANGE_REQUESTED
- self._blockedByKeyExchange = []
- def _allowedKeyExchangeMessageType(self, messageType):
- """
- Determine if the given message type may be sent while key exchange is
- in progress.
- @param messageType: The type of message
- @type messageType: L{int}
- @return: C{True} if the given type of message may be sent while key
- exchange is in progress, C{False} if it may not.
- @rtype: L{bool}
- @see: U{http://tools.ietf.org/html/rfc4253#section-7.1}
- """
- # Written somewhat peculularly to reflect the way the specification
- # defines the allowed message types.
- if 1 <= messageType <= 19:
- return messageType not in (
- MSG_SERVICE_REQUEST,
- MSG_SERVICE_ACCEPT,
- MSG_EXT_INFO,
- )
- if 20 <= messageType <= 29:
- return messageType not in (MSG_KEXINIT,)
- return 30 <= messageType <= 49
- def sendPacket(self, messageType, payload):
- """
- Sends a packet. If it's been set up, compress the data, encrypt it,
- and authenticate it before sending. If key exchange is in progress and
- the message is not part of key exchange, queue it to be sent later.
- @param messageType: The type of the packet; generally one of the
- MSG_* values.
- @type messageType: L{int}
- @param payload: The payload for the message.
- @type payload: L{str}
- """
- if self._keyExchangeState != self._KEY_EXCHANGE_NONE:
- if not self._allowedKeyExchangeMessageType(messageType):
- self._blockedByKeyExchange.append((messageType, payload))
- return
- payload = bytes((messageType,)) + payload
- if self.outgoingCompression:
- payload = self.outgoingCompression.compress(
- payload
- ) + self.outgoingCompression.flush(2)
- bs = self.currentEncryptions.encBlockSize
- # 4 for the packet length and 1 for the padding length
- totalSize = 5 + len(payload)
- lenPad = bs - (totalSize % bs)
- if lenPad < 4:
- lenPad = lenPad + bs
- packet = (
- struct.pack("!LB", totalSize + lenPad - 4, lenPad)
- + payload
- + randbytes.secureRandom(lenPad)
- )
- encPacket = self.currentEncryptions.encrypt(
- packet
- ) + self.currentEncryptions.makeMAC(self.outgoingPacketSequence, packet)
- self.transport.write(encPacket)
- self.outgoingPacketSequence += 1
- def getPacket(self):
- """
- Try to return a decrypted, authenticated, and decompressed packet
- out of the buffer. If there is not enough data, return None.
- @rtype: L{str} or L{None}
- @return: The decoded packet, if any.
- """
- bs = self.currentEncryptions.decBlockSize
- ms = self.currentEncryptions.verifyDigestSize
- if len(self.buf) < bs:
- # Not enough data for a block
- return
- if not hasattr(self, "first"):
- first = self.currentEncryptions.decrypt(self.buf[:bs])
- else:
- first = self.first
- del self.first
- packetLen, paddingLen = struct.unpack("!LB", first[:5])
- if packetLen > 1048576: # 1024 ** 2
- self.sendDisconnect(
- DISCONNECT_PROTOCOL_ERROR,
- networkString(f"bad packet length {packetLen}"),
- )
- return
- if len(self.buf) < packetLen + 4 + ms:
- # Not enough data for a packet
- self.first = first
- return
- if (packetLen + 4) % bs != 0:
- self.sendDisconnect(
- DISCONNECT_PROTOCOL_ERROR,
- networkString(
- "bad packet mod (%i%%%i == %i)"
- % (packetLen + 4, bs, (packetLen + 4) % bs)
- ),
- )
- return
- encData, self.buf = self.buf[: 4 + packetLen], self.buf[4 + packetLen :]
- packet = first + self.currentEncryptions.decrypt(encData[bs:])
- if len(packet) != 4 + packetLen:
- self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, b"bad decryption")
- return
- if ms:
- macData, self.buf = self.buf[:ms], self.buf[ms:]
- if not self.currentEncryptions.verify(
- self.incomingPacketSequence, packet, macData
- ):
- self.sendDisconnect(DISCONNECT_MAC_ERROR, b"bad MAC")
- return
- payload = packet[5:-paddingLen]
- if self.incomingCompression:
- try:
- payload = self.incomingCompression.decompress(payload)
- except Exception:
- # Tolerate any errors in decompression
- self._log.failure("Error decompressing payload")
- self.sendDisconnect(DISCONNECT_COMPRESSION_ERROR, b"compression error")
- return
- self.incomingPacketSequence += 1
- return payload
- def _unsupportedVersionReceived(self, remoteVersion):
- """
- Called when an unsupported version of the ssh protocol is received from
- the remote endpoint.
- @param remoteVersion: remote ssh protocol version which is unsupported
- by us.
- @type remoteVersion: L{str}
- """
- self.sendDisconnect(
- DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, b"bad version " + remoteVersion
- )
- def dataReceived(self, data):
- """
- First, check for the version string (SSH-2.0-*). After that has been
- received, this method adds data to the buffer, and pulls out any
- packets.
- @type data: L{bytes}
- @param data: The data that was received.
- """
- self.buf = self.buf + data
- if not self.gotVersion:
- if len(self.buf) > 4096:
- self.sendDisconnect(
- DISCONNECT_CONNECTION_LOST,
- b"Peer version string longer than 4KB. "
- b"Preventing a denial of service attack.",
- )
- return
- if self.buf.find(b"\n", self.buf.find(b"SSH-")) == -1:
- return
- # RFC 4253 section 4.2 ask for strict `\r\n` line ending.
- # Here we are a bit more relaxed and accept implementations ending
- # only in '\n'.
- # https://tools.ietf.org/html/rfc4253#section-4.2
- lines = self.buf.split(b"\n")
- for p in lines:
- if p.startswith(b"SSH-"):
- self.gotVersion = True
- # Since the line was split on '\n' and most of the time
- # it uses '\r\n' we may get an extra '\r'.
- self.otherVersionString = p.rstrip(b"\r")
- remoteVersion = p.split(b"-")[1]
- if remoteVersion not in self.supportedVersions:
- self._unsupportedVersionReceived(remoteVersion)
- return
- i = lines.index(p)
- self.buf = b"\n".join(lines[i + 1 :])
- packet = self.getPacket()
- while packet:
- messageNum = ord(packet[0:1])
- self.dispatchMessage(messageNum, packet[1:])
- packet = self.getPacket()
- def dispatchMessage(self, messageNum, payload):
- """
- Send a received message to the appropriate method.
- @type messageNum: L{int}
- @param messageNum: The message number.
- @type payload: L{bytes}
- @param payload: The message payload.
- """
- if messageNum < 50 and messageNum in messages:
- messageType = messages[messageNum][4:]
- f = getattr(self, f"ssh_{messageType}", None)
- if f is not None:
- f(payload)
- else:
- self._log.debug(
- "couldn't handle {messageType}: {payload!r}",
- messageType=messageType,
- payload=payload,
- )
- self.sendUnimplemented()
- elif self.service:
- self.service.packetReceived(messageNum, payload)
- else:
- self._log.debug(
- "couldn't handle {messageNum}: {payload!r}",
- messageNum=messageNum,
- payload=payload,
- )
- self.sendUnimplemented()
- def getPeer(self):
- """
- Returns an L{SSHTransportAddress} corresponding to the other (peer)
- side of this transport.
- @return: L{SSHTransportAddress} for the peer
- @rtype: L{SSHTransportAddress}
- @since: 12.1
- """
- return address.SSHTransportAddress(self.transport.getPeer())
- def getHost(self):
- """
- Returns an L{SSHTransportAddress} corresponding to the this side of
- transport.
- @return: L{SSHTransportAddress} for the peer
- @rtype: L{SSHTransportAddress}
- @since: 12.1
- """
- return address.SSHTransportAddress(self.transport.getHost())
- @property
- def kexAlg(self):
- """
- The key exchange algorithm name agreed between client and server.
- """
- return self._kexAlg
- @kexAlg.setter
- def kexAlg(self, value):
- """
- Set the key exchange algorithm name.
- """
- self._kexAlg = value
- # Client-initiated rekeying looks like this:
- #
- # C> MSG_KEXINIT
- # S> MSG_KEXINIT
- # C> MSG_KEX_DH_GEX_REQUEST or MSG_KEXDH_INIT
- # S> MSG_KEX_DH_GEX_GROUP or MSG_KEXDH_REPLY
- # C> MSG_KEX_DH_GEX_INIT or --
- # S> MSG_KEX_DH_GEX_REPLY or --
- # C> MSG_NEWKEYS
- # S> MSG_NEWKEYS
- #
- # Server-initiated rekeying is the same, only the first two messages are
- # switched.
- def ssh_KEXINIT(self, packet):
- """
- Called when we receive a MSG_KEXINIT message. Payload::
- bytes[16] cookie
- string keyExchangeAlgorithms
- string keyAlgorithms
- string incomingEncryptions
- string outgoingEncryptions
- string incomingAuthentications
- string outgoingAuthentications
- string incomingCompressions
- string outgoingCompressions
- string incomingLanguages
- string outgoingLanguages
- bool firstPacketFollows
- unit32 0 (reserved)
- Starts setting up the key exchange, keys, encryptions, and
- authentications. Extended by ssh_KEXINIT in SSHServerTransport and
- SSHClientTransport.
- @type packet: L{bytes}
- @param packet: The message data.
- @return: A L{tuple} of negotiated key exchange algorithms, key
- algorithms, and unhandled data, or L{None} if something went wrong.
- """
- self.otherKexInitPayload = bytes((MSG_KEXINIT,)) + packet
- # This is useless to us:
- # cookie = packet[: 16]
- k = getNS(packet[16:], 10)
- strings, rest = k[:-1], k[-1]
- (
- kexAlgs,
- keyAlgs,
- encCS,
- encSC,
- macCS,
- macSC,
- compCS,
- compSC,
- langCS,
- langSC,
- ) = (s.split(b",") for s in strings)
- # These are the server directions
- outs = [encSC, macSC, compSC]
- ins = [encCS, macCS, compCS]
- if self.isClient:
- outs, ins = ins, outs # Switch directions
- server = (
- self.supportedKeyExchanges,
- self.supportedPublicKeys,
- self.supportedCiphers,
- self.supportedCiphers,
- self.supportedMACs,
- self.supportedMACs,
- self.supportedCompressions,
- self.supportedCompressions,
- )
- client = (kexAlgs, keyAlgs, outs[0], ins[0], outs[1], ins[1], outs[2], ins[2])
- if self.isClient:
- server, client = client, server
- self.kexAlg = ffs(client[0], server[0])
- self.keyAlg = ffs(client[1], server[1])
- self.nextEncryptions = SSHCiphers(
- ffs(client[2], server[2]),
- ffs(client[3], server[3]),
- ffs(client[4], server[4]),
- ffs(client[5], server[5]),
- )
- self.outgoingCompressionType = ffs(client[6], server[6])
- self.incomingCompressionType = ffs(client[7], server[7])
- if (
- None
- in (
- self.kexAlg,
- self.keyAlg,
- self.outgoingCompressionType,
- self.incomingCompressionType,
- )
- # We MUST disconnect if an extension negotiation indication ends
- # up being negotiated as a key exchange method (RFC 8308,
- # section 2.2).
- or self.kexAlg in (self._EXT_INFO_C, self._EXT_INFO_S)
- ):
- self.sendDisconnect(
- DISCONNECT_KEY_EXCHANGE_FAILED, b"couldn't match all kex parts"
- )
- return
- if None in self.nextEncryptions.__dict__.values():
- self.sendDisconnect(
- DISCONNECT_KEY_EXCHANGE_FAILED, b"couldn't match all kex parts"
- )
- return
- self._peerSupportsExtensions = (
- self._EXT_INFO_S if self.isClient else self._EXT_INFO_C
- ) in kexAlgs
- self._log.debug(
- "kex alg={kexAlg!r} key alg={keyAlg!r}",
- kexAlg=self.kexAlg,
- keyAlg=self.keyAlg,
- )
- self._log.debug(
- "outgoing: {cip!r} {mac!r} {compression!r}",
- cip=self.nextEncryptions.outCipType,
- mac=self.nextEncryptions.outMACType,
- compression=self.outgoingCompressionType,
- )
- self._log.debug(
- "incoming: {cip!r} {mac!r} {compression!r}",
- cip=self.nextEncryptions.inCipType,
- mac=self.nextEncryptions.inMACType,
- compression=self.incomingCompressionType,
- )
- if self._keyExchangeState == self._KEY_EXCHANGE_REQUESTED:
- self._keyExchangeState = self._KEY_EXCHANGE_PROGRESSING
- else:
- self.sendKexInit()
- return kexAlgs, keyAlgs, rest # For SSHServerTransport to use
- def ssh_DISCONNECT(self, packet):
- """
- Called when we receive a MSG_DISCONNECT message. Payload::
- long code
- string description
- This means that the other side has disconnected. Pass the message up
- and disconnect ourselves.
- @type packet: L{bytes}
- @param packet: The message data.
- """
- reasonCode = struct.unpack(">L", packet[:4])[0]
- description, foo = getNS(packet[4:])
- self.receiveError(reasonCode, description)
- self.transport.loseConnection()
- def ssh_IGNORE(self, packet):
- """
- Called when we receive a MSG_IGNORE message. No payload.
- This means nothing; we simply return.
- @type packet: L{bytes}
- @param packet: The message data.
- """
- def ssh_UNIMPLEMENTED(self, packet):
- """
- Called when we receive a MSG_UNIMPLEMENTED message. Payload::
- long packet
- This means that the other side did not implement one of our packets.
- @type packet: L{bytes}
- @param packet: The message data.
- """
- (seqnum,) = struct.unpack(">L", packet)
- self.receiveUnimplemented(seqnum)
- def ssh_DEBUG(self, packet):
- """
- Called when we receive a MSG_DEBUG message. Payload::
- bool alwaysDisplay
- string message
- string language
- This means the other side has passed along some debugging info.
- @type packet: L{bytes}
- @param packet: The message data.
- """
- alwaysDisplay = bool(ord(packet[0:1]))
- message, lang, foo = getNS(packet[1:], 2)
- self.receiveDebug(alwaysDisplay, message, lang)
- def ssh_EXT_INFO(self, packet):
- """
- Called when we get a MSG_EXT_INFO message. Payload::
- uint32 nr-extensions
- repeat the following 2 fields "nr-extensions" times:
- string extension-name
- string extension-value (binary)
- @type packet: L{bytes}
- @param packet: The message data.
- """
- (numExtensions,) = struct.unpack(">L", packet[:4])
- packet = packet[4:]
- extensions = {}
- for _ in range(numExtensions):
- extName, extValue, packet = getNS(packet, 2)
- extensions[extName] = extValue
- self.peerExtensions = extensions
- def setService(self, service):
- """
- Set our service to service and start it running. If we were
- running a service previously, stop it first.
- @type service: C{SSHService}
- @param service: The service to attach.
- """
- self._log.debug("starting service {service!r}", service=service.name)
- if self.service:
- self.service.serviceStopped()
- self.service = service
- service.transport = self
- self.service.serviceStarted()
- def sendDebug(self, message, alwaysDisplay=False, language=b""):
- """
- Send a debug message to the other side.
- @param message: the message to send.
- @type message: L{str}
- @param alwaysDisplay: if True, tell the other side to always
- display this message.
- @type alwaysDisplay: L{bool}
- @param language: optionally, the language the message is in.
- @type language: L{str}
- """
- self.sendPacket(
- MSG_DEBUG, (b"\1" if alwaysDisplay else b"\0") + NS(message) + NS(language)
- )
- def sendIgnore(self, message):
- """
- Send a message that will be ignored by the other side. This is
- useful to fool attacks based on guessing packet sizes in the
- encrypted stream.
- @param message: data to send with the message
- @type message: L{str}
- """
- self.sendPacket(MSG_IGNORE, NS(message))
- def sendUnimplemented(self):
- """
- Send a message to the other side that the last packet was not
- understood.
- """
- seqnum = self.incomingPacketSequence
- self.sendPacket(MSG_UNIMPLEMENTED, struct.pack("!L", seqnum))
- def sendDisconnect(self, reason, desc):
- """
- Send a disconnect message to the other side and then disconnect.
- @param reason: the reason for the disconnect. Should be one of the
- DISCONNECT_* values.
- @type reason: L{int}
- @param desc: a descrption of the reason for the disconnection.
- @type desc: L{str}
- """
- self.sendPacket(MSG_DISCONNECT, struct.pack(">L", reason) + NS(desc) + NS(b""))
- self._log.info(
- "Disconnecting with error, code {code}\nreason: {description}",
- code=reason,
- description=desc,
- )
- self.transport.loseConnection()
- def sendExtInfo(self, extensions):
- """
- Send an RFC 8308 extension advertisement to the remote peer.
- Nothing is sent if the peer doesn't support negotiations.
- @type extensions: L{list} of (L{bytes}, L{bytes})
- @param extensions: a list of (extension-name, extension-value) pairs.
- """
- if self._peerSupportsExtensions:
- payload = b"".join(
- [struct.pack(">L", len(extensions))]
- + [NS(name) + NS(value) for name, value in extensions]
- )
- self.sendPacket(MSG_EXT_INFO, payload)
- def _startEphemeralDH(self):
- """
- Prepares for a Diffie-Hellman key agreement exchange.
- Creates an ephemeral keypair in the group defined by (self.g,
- self.p) and stores it.
- """
- numbers = dh.DHParameterNumbers(self.p, self.g)
- parameters = numbers.parameters(default_backend())
- self.dhSecretKey = parameters.generate_private_key()
- y = self.dhSecretKey.public_key().public_numbers().y
- self.dhSecretKeyPublicMP = MP(y)
- def _finishEphemeralDH(self, remoteDHpublicKey):
- """
- Completes the Diffie-Hellman key agreement started by
- _startEphemeralDH, and forgets the ephemeral secret key.
- @type remoteDHpublicKey: L{int}
- @rtype: L{bytes}
- @return: The new shared secret, in SSH C{mpint} format.
- """
- remoteKey = dh.DHPublicNumbers(
- remoteDHpublicKey, dh.DHParameterNumbers(self.p, self.g)
- ).public_key(default_backend())
- secret = self.dhSecretKey.exchange(remoteKey)
- del self.dhSecretKey
- # The result of a Diffie-Hellman exchange is an integer, but
- # the Cryptography module returns it as bytes in a form that
- # is only vaguely documented. We fix it up to match the SSH
- # MP-integer format as described in RFC4251.
- secret = secret.lstrip(b"\x00")
- ch = ord(secret[0:1])
- if ch & 0x80: # High bit set?
- # Make room for the sign bit
- prefix = struct.pack(">L", len(secret) + 1) + b"\x00"
- else:
- prefix = struct.pack(">L", len(secret))
- return prefix + secret
- def _getKey(self, c, sharedSecret, exchangeHash):
- """
- Get one of the keys for authentication/encryption.
- @type c: L{bytes}
- @param c: The letter identifying which key this is.
- @type sharedSecret: L{bytes}
- @param sharedSecret: The shared secret K.
- @type exchangeHash: L{bytes}
- @param exchangeHash: The hash H from key exchange.
- @rtype: L{bytes}
- @return: The derived key.
- """
- hashProcessor = _kex.getHashProcessor(self.kexAlg)
- k1 = hashProcessor(sharedSecret + exchangeHash + c + self.sessionID)
- k1 = k1.digest()
- k2 = hashProcessor(sharedSecret + exchangeHash + k1).digest()
- k3 = hashProcessor(sharedSecret + exchangeHash + k1 + k2).digest()
- k4 = hashProcessor(sharedSecret + exchangeHash + k1 + k2 + k3).digest()
- return k1 + k2 + k3 + k4
- def _keySetup(self, sharedSecret, exchangeHash):
- """
- Set up the keys for the connection and sends MSG_NEWKEYS when
- finished,
- @param sharedSecret: a secret string agreed upon using a Diffie-
- Hellman exchange, so it is only shared between
- the server and the client.
- @type sharedSecret: L{str}
- @param exchangeHash: A hash of various data known by both sides.
- @type exchangeHash: L{str}
- """
- if not self.sessionID:
- self.sessionID = exchangeHash
- initIVCS = self._getKey(b"A", sharedSecret, exchangeHash)
- initIVSC = self._getKey(b"B", sharedSecret, exchangeHash)
- encKeyCS = self._getKey(b"C", sharedSecret, exchangeHash)
- encKeySC = self._getKey(b"D", sharedSecret, exchangeHash)
- integKeyCS = self._getKey(b"E", sharedSecret, exchangeHash)
- integKeySC = self._getKey(b"F", sharedSecret, exchangeHash)
- outs = [initIVSC, encKeySC, integKeySC]
- ins = [initIVCS, encKeyCS, integKeyCS]
- if self.isClient: # Reverse for the client
- outs, ins = ins, outs
- self.nextEncryptions.setKeys(outs[0], outs[1], ins[0], ins[1], outs[2], ins[2])
- self.sendPacket(MSG_NEWKEYS, b"")
- def _newKeys(self):
- """
- Called back by a subclass once a I{MSG_NEWKEYS} message has been
- received. This indicates key exchange has completed and new encryption
- and compression parameters should be adopted. Any messages which were
- queued during key exchange will also be flushed.
- """
- self._log.debug("NEW KEYS")
- self.currentEncryptions = self.nextEncryptions
- if self.outgoingCompressionType == b"zlib":
- self.outgoingCompression = zlib.compressobj(6)
- if self.incomingCompressionType == b"zlib":
- self.incomingCompression = zlib.decompressobj()
- self._keyExchangeState = self._KEY_EXCHANGE_NONE
- messages = self._blockedByKeyExchange
- self._blockedByKeyExchange = None
- for messageType, payload in messages:
- self.sendPacket(messageType, payload)
- def isEncrypted(self, direction="out"):
- """
- Check if the connection is encrypted in the given direction.
- @type direction: L{str}
- @param direction: The direction: one of 'out', 'in', or 'both'.
- @rtype: L{bool}
- @return: C{True} if it is encrypted.
- """
- if direction == "out":
- return self.currentEncryptions.outCipType != b"none"
- elif direction == "in":
- return self.currentEncryptions.inCipType != b"none"
- elif direction == "both":
- return self.isEncrypted("in") and self.isEncrypted("out")
- else:
- raise TypeError('direction must be "out", "in", or "both"')
- def isVerified(self, direction="out"):
- """
- Check if the connection is verified/authentication in the given direction.
- @type direction: L{str}
- @param direction: The direction: one of 'out', 'in', or 'both'.
- @rtype: L{bool}
- @return: C{True} if it is verified.
- """
- if direction == "out":
- return self.currentEncryptions.outMACType != b"none"
- elif direction == "in":
- return self.currentEncryptions.inMACType != b"none"
- elif direction == "both":
- return self.isVerified("in") and self.isVerified("out")
- else:
- raise TypeError('direction must be "out", "in", or "both"')
- def loseConnection(self):
- """
- Lose the connection to the other side, sending a
- DISCONNECT_CONNECTION_LOST message.
- """
- self.sendDisconnect(DISCONNECT_CONNECTION_LOST, b"user closed connection")
- # Client methods
- def receiveError(self, reasonCode, description):
- """
- Called when we receive a disconnect error message from the other
- side.
- @param reasonCode: the reason for the disconnect, one of the
- DISCONNECT_ values.
- @type reasonCode: L{int}
- @param description: a human-readable description of the
- disconnection.
- @type description: L{str}
- """
- self._log.error(
- "Got remote error, code {code}\nreason: {description}",
- code=reasonCode,
- description=description,
- )
- def receiveUnimplemented(self, seqnum):
- """
- Called when we receive an unimplemented packet message from the other
- side.
- @param seqnum: the sequence number that was not understood.
- @type seqnum: L{int}
- """
- self._log.warn("other side unimplemented packet #{seqnum}", seqnum=seqnum)
- def receiveDebug(self, alwaysDisplay, message, lang):
- """
- Called when we receive a debug message from the other side.
- @param alwaysDisplay: if True, this message should always be
- displayed.
- @type alwaysDisplay: L{bool}
- @param message: the debug message
- @type message: L{str}
- @param lang: optionally the language the message is in.
- @type lang: L{str}
- """
- if alwaysDisplay:
- self._log.debug("Remote Debug Message: {message}", message=message)
- def _generateECPrivateKey(self):
- """
- Generate an private key for ECDH key exchange.
- @rtype: The appropriate private key type matching C{self.kexAlg}:
- L{ec.EllipticCurvePrivateKey} for C{ecdh-sha2-nistp*}, or
- L{x25519.X25519PrivateKey} for C{curve25519-sha256}.
- @return: The generated private key.
- """
- if self.kexAlg.startswith(b"ecdh-sha2-nistp"):
- try:
- curve = keys._curveTable[b"ecdsa" + self.kexAlg[4:]]
- except KeyError:
- raise UnsupportedAlgorithm("unused-key")
- return ec.generate_private_key(curve, default_backend())
- elif self.kexAlg in (b"curve25519-sha256", b"curve25519-sha256@libssh.org"):
- return x25519.X25519PrivateKey.generate()
- else:
- raise UnsupportedAlgorithm(
- "Cannot generate elliptic curve private key for {!r}".format(
- self.kexAlg
- )
- )
- def _encodeECPublicKey(self, ecPub):
- """
- Encode an elliptic curve public key to bytes.
- @type ecPub: The appropriate public key type matching
- C{self.kexAlg}: L{ec.EllipticCurvePublicKey} for
- C{ecdh-sha2-nistp*}, or L{x25519.X25519PublicKey} for
- C{curve25519-sha256}.
- @param ecPub: The public key to encode.
- @rtype: L{bytes}
- @return: The encoded public key.
- """
- if self.kexAlg.startswith(b"ecdh-sha2-nistp"):
- return ecPub.public_bytes(
- serialization.Encoding.X962,
- serialization.PublicFormat.UncompressedPoint,
- )
- elif self.kexAlg in (b"curve25519-sha256", b"curve25519-sha256@libssh.org"):
- return ecPub.public_bytes(
- serialization.Encoding.Raw, serialization.PublicFormat.Raw
- )
- else:
- raise UnsupportedAlgorithm(
- f"Cannot encode elliptic curve public key for {self.kexAlg!r}"
- )
- def _generateECSharedSecret(self, ecPriv, theirECPubBytes):
- """
- Generate a shared secret for ECDH key exchange.
- @type ecPriv: The appropriate private key type matching
- C{self.kexAlg}: L{ec.EllipticCurvePrivateKey} for
- C{ecdh-sha2-nistp*}, or L{x25519.X25519PrivateKey} for
- C{curve25519-sha256}.
- @param ecPriv: Our private key.
- @rtype: L{bytes}
- @return: The generated shared secret, as an SSH multiple-precision
- integer.
- """
- if self.kexAlg.startswith(b"ecdh-sha2-nistp"):
- try:
- curve = keys._curveTable[b"ecdsa" + self.kexAlg[4:]]
- except KeyError:
- raise UnsupportedAlgorithm("unused-key")
- theirECPub = ec.EllipticCurvePublicKey.from_encoded_point(
- curve, theirECPubBytes
- )
- sharedSecret = ecPriv.exchange(ec.ECDH(), theirECPub)
- elif self.kexAlg in (b"curve25519-sha256", b"curve25519-sha256@libssh.org"):
- theirECPub = x25519.X25519PublicKey.from_public_bytes(theirECPubBytes)
- sharedSecret = ecPriv.exchange(theirECPub)
- else:
- raise UnsupportedAlgorithm(
- "Cannot generate elliptic curve shared secret for {!r}".format(
- self.kexAlg
- )
- )
- return _mpFromBytes(sharedSecret)
- class SSHServerTransport(SSHTransportBase):
- """
- SSHServerTransport implements the server side of the SSH protocol.
- @ivar isClient: since we are never the client, this is always False.
- @ivar ignoreNextPacket: if True, ignore the next key exchange packet. This
- is set when the client sends a guessed key exchange packet but with
- an incorrect guess.
- @ivar dhGexRequest: the KEX_DH_GEX_REQUEST(_OLD) that the client sent.
- The key generation needs this to be stored.
- @ivar g: the Diffie-Hellman group generator.
- @ivar p: the Diffie-Hellman group prime.
- """
- isClient = False
- ignoreNextPacket = 0
- def _getHostKeys(self, keyAlg):
- """
- Get the public and private host keys corresponding to the given
- public key signature algorithm.
- The factory stores public and private host keys by their key format,
- which is not quite the same as the key signature algorithm: for
- example, an ssh-rsa key can sign using any of the ssh-rsa,
- rsa-sha2-256, or rsa-sha2-512 algorithms.
- @type keyAlg: L{bytes}
- @param keyAlg: A public key signature algorithm name.
- @rtype: 2-L{tuple} of L{keys.Key}
- @return: The public and private host keys.
- @raises KeyError: if the factory does not have both a public and a
- private host key for this signature algorithm.
- """
- if keyAlg in {b"rsa-sha2-256", b"rsa-sha2-512"}:
- keyFormat = b"ssh-rsa"
- else:
- keyFormat = keyAlg
- return self.factory.publicKeys[keyFormat], self.factory.privateKeys[keyFormat]
- def ssh_KEXINIT(self, packet):
- """
- Called when we receive a MSG_KEXINIT message. For a description
- of the packet, see SSHTransportBase.ssh_KEXINIT(). Additionally,
- this method checks if a guessed key exchange packet was sent. If
- it was sent, and it guessed incorrectly, the next key exchange
- packet MUST be ignored.
- """
- retval = SSHTransportBase.ssh_KEXINIT(self, packet)
- if not retval: # Disconnected
- return
- else:
- kexAlgs, keyAlgs, rest = retval
- if ord(rest[0:1]): # Flag first_kex_packet_follows?
- if (
- kexAlgs[0] != self.supportedKeyExchanges[0]
- or keyAlgs[0] != self.supportedPublicKeys[0]
- ):
- self.ignoreNextPacket = True # Guess was wrong
- def _ssh_KEX_ECDH_INIT(self, packet):
- """
- Called from L{ssh_KEX_DH_GEX_REQUEST_OLD} to handle
- elliptic curve key exchanges.
- Payload::
- string client Elliptic Curve Diffie-Hellman public key
- Just like L{_ssh_KEXDH_INIT} this message type is also not dispatched
- directly. Extra check to determine if this is really KEX_ECDH_INIT
- is required.
- First we load the host's public/private keys.
- Then we generate the ECDH public/private keypair for the given curve.
- With that we generate the shared secret key.
- Then we compute the hash to sign and send back to the client
- Along with the server's public key and the ECDH public key.
- @type packet: L{bytes}
- @param packet: The message data.
- @return: None.
- """
- # Get the raw client public key.
- pktPub, packet = getNS(packet)
- # Get the host's public and private keys
- pubHostKey, privHostKey = self._getHostKeys(self.keyAlg)
- # Generate the private key
- ecPriv = self._generateECPrivateKey()
- # Get the public key
- self.ecPub = ecPriv.public_key()
- encPub = self._encodeECPublicKey(self.ecPub)
- # Generate the shared secret
- sharedSecret = self._generateECSharedSecret(ecPriv, pktPub)
- # Finish update and digest
- h = _kex.getHashProcessor(self.kexAlg)()
- h.update(NS(self.otherVersionString))
- h.update(NS(self.ourVersionString))
- h.update(NS(self.otherKexInitPayload))
- h.update(NS(self.ourKexInitPayload))
- h.update(NS(pubHostKey.blob()))
- h.update(NS(pktPub))
- h.update(NS(encPub))
- h.update(sharedSecret)
- exchangeHash = h.digest()
- self.sendPacket(
- MSG_KEXDH_REPLY,
- NS(pubHostKey.blob())
- + NS(encPub)
- + NS(privHostKey.sign(exchangeHash, signatureType=self.keyAlg)),
- )
- self._keySetup(sharedSecret, exchangeHash)
- def _ssh_KEXDH_INIT(self, packet):
- """
- Called to handle the beginning of a non-group key exchange.
- Unlike other message types, this is not dispatched automatically. It
- is called from C{ssh_KEX_DH_GEX_REQUEST_OLD} because an extra check is
- required to determine if this is really a KEXDH_INIT message or if it
- is a KEX_DH_GEX_REQUEST_OLD message.
- The KEXDH_INIT payload::
- integer e (the client's Diffie-Hellman public key)
- We send the KEXDH_REPLY with our host key and signature.
- @type packet: L{bytes}
- @param packet: The message data.
- """
- clientDHpublicKey, foo = getMP(packet)
- pubHostKey, privHostKey = self._getHostKeys(self.keyAlg)
- self.g, self.p = _kex.getDHGeneratorAndPrime(self.kexAlg)
- self._startEphemeralDH()
- sharedSecret = self._finishEphemeralDH(clientDHpublicKey)
- h = _kex.getHashProcessor(self.kexAlg)()
- h.update(NS(self.otherVersionString))
- h.update(NS(self.ourVersionString))
- h.update(NS(self.otherKexInitPayload))
- h.update(NS(self.ourKexInitPayload))
- h.update(NS(pubHostKey.blob()))
- h.update(MP(clientDHpublicKey))
- h.update(self.dhSecretKeyPublicMP)
- h.update(sharedSecret)
- exchangeHash = h.digest()
- self.sendPacket(
- MSG_KEXDH_REPLY,
- NS(pubHostKey.blob())
- + self.dhSecretKeyPublicMP
- + NS(privHostKey.sign(exchangeHash, signatureType=self.keyAlg)),
- )
- self._keySetup(sharedSecret, exchangeHash)
- def ssh_KEX_DH_GEX_REQUEST_OLD(self, packet):
- """
- This represents different key exchange methods that share the same
- integer value. If the message is determined to be a KEXDH_INIT,
- L{_ssh_KEXDH_INIT} is called to handle it. If it is a KEX_ECDH_INIT,
- L{_ssh_KEX_ECDH_INIT} is called.
- Otherwise, for KEX_DH_GEX_REQUEST_OLD payload::
- integer ideal (ideal size for the Diffie-Hellman prime)
- We send the KEX_DH_GEX_GROUP message with the group that is
- closest in size to ideal.
- If we were told to ignore the next key exchange packet by ssh_KEXINIT,
- drop it on the floor and return.
- @type packet: L{bytes}
- @param packet: The message data.
- """
- if self.ignoreNextPacket:
- self.ignoreNextPacket = 0
- return
- # KEXDH_INIT, KEX_ECDH_INIT, and KEX_DH_GEX_REQUEST_OLD
- # have the same value, so use another cue
- # to decide what kind of message the peer sent us.
- if _kex.isFixedGroup(self.kexAlg):
- return self._ssh_KEXDH_INIT(packet)
- elif _kex.isEllipticCurve(self.kexAlg):
- return self._ssh_KEX_ECDH_INIT(packet)
- else:
- self.dhGexRequest = packet
- ideal = struct.unpack(">L", packet)[0]
- self.g, self.p = self.factory.getDHPrime(ideal)
- self._startEphemeralDH()
- self.sendPacket(MSG_KEX_DH_GEX_GROUP, MP(self.p) + MP(self.g))
- def ssh_KEX_DH_GEX_REQUEST(self, packet):
- """
- Called when we receive a MSG_KEX_DH_GEX_REQUEST message. Payload::
- integer minimum
- integer ideal
- integer maximum
- The client is asking for a Diffie-Hellman group between minimum and
- maximum size, and close to ideal if possible. We reply with a
- MSG_KEX_DH_GEX_GROUP message.
- If we were told to ignore the next key exchange packet by ssh_KEXINIT,
- drop it on the floor and return.
- @type packet: L{bytes}
- @param packet: The message data.
- """
- if self.ignoreNextPacket:
- self.ignoreNextPacket = 0
- return
- self.dhGexRequest = packet
- min, ideal, max = struct.unpack(">3L", packet)
- self.g, self.p = self.factory.getDHPrime(ideal)
- self._startEphemeralDH()
- self.sendPacket(MSG_KEX_DH_GEX_GROUP, MP(self.p) + MP(self.g))
- def ssh_KEX_DH_GEX_INIT(self, packet):
- """
- Called when we get a MSG_KEX_DH_GEX_INIT message. Payload::
- integer e (client DH public key)
- We send the MSG_KEX_DH_GEX_REPLY message with our host key and
- signature.
- @type packet: L{bytes}
- @param packet: The message data.
- """
- clientDHpublicKey, foo = getMP(packet)
- pubHostKey, privHostKey = self._getHostKeys(self.keyAlg)
- # TODO: we should also look at the value they send to us and reject
- # insecure values of f (if g==2 and f has a single '1' bit while the
- # rest are '0's, then they must have used a small y also).
- # TODO: This could be computed when self.p is set up
- # or do as openssh does and scan f for a single '1' bit instead
- sharedSecret = self._finishEphemeralDH(clientDHpublicKey)
- h = _kex.getHashProcessor(self.kexAlg)()
- h.update(NS(self.otherVersionString))
- h.update(NS(self.ourVersionString))
- h.update(NS(self.otherKexInitPayload))
- h.update(NS(self.ourKexInitPayload))
- h.update(NS(pubHostKey.blob()))
- h.update(self.dhGexRequest)
- h.update(MP(self.p))
- h.update(MP(self.g))
- h.update(MP(clientDHpublicKey))
- h.update(self.dhSecretKeyPublicMP)
- h.update(sharedSecret)
- exchangeHash = h.digest()
- self.sendPacket(
- MSG_KEX_DH_GEX_REPLY,
- NS(pubHostKey.blob())
- + self.dhSecretKeyPublicMP
- + NS(privHostKey.sign(exchangeHash, signatureType=self.keyAlg)),
- )
- self._keySetup(sharedSecret, exchangeHash)
- def _keySetup(self, sharedSecret, exchangeHash):
- """
- See SSHTransportBase._keySetup().
- """
- firstKey = self.sessionID is None
- SSHTransportBase._keySetup(self, sharedSecret, exchangeHash)
- # RFC 8308 section 2.4 says that the server MAY send EXT_INFO at
- # zero, one, or both of the following opportunities: the next packet
- # following the server's first MSG_NEWKEYS, or immediately preceding
- # the server's MSG_USERAUTH_SUCCESS. We have no need for the
- # latter, so make sure we only send it in the former case.
- if firstKey:
- self.sendExtInfo(
- [(b"server-sig-algs", b",".join(self.supportedPublicKeys))]
- )
- def ssh_NEWKEYS(self, packet):
- """
- Called when we get a MSG_NEWKEYS message. No payload.
- When we get this, the keys have been set on both sides, and we
- start using them to encrypt and authenticate the connection.
- @type packet: L{bytes}
- @param packet: The message data.
- """
- if packet != b"":
- self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, b"NEWKEYS takes no data")
- return
- self._newKeys()
- def ssh_SERVICE_REQUEST(self, packet):
- """
- Called when we get a MSG_SERVICE_REQUEST message. Payload::
- string serviceName
- The client has requested a service. If we can start the service,
- start it; otherwise, disconnect with
- DISCONNECT_SERVICE_NOT_AVAILABLE.
- @type packet: L{bytes}
- @param packet: The message data.
- """
- service, rest = getNS(packet)
- cls = self.factory.getService(self, service)
- if not cls:
- self.sendDisconnect(
- DISCONNECT_SERVICE_NOT_AVAILABLE, b"don't have service " + service
- )
- return
- else:
- self.sendPacket(MSG_SERVICE_ACCEPT, NS(service))
- self.setService(cls())
- class SSHClientTransport(SSHTransportBase):
- """
- SSHClientTransport implements the client side of the SSH protocol.
- @ivar isClient: since we are always the client, this is always True.
- @ivar _gotNewKeys: if we receive a MSG_NEWKEYS message before we are
- ready to transition to the new keys, this is set to True so we
- can transition when the keys are ready locally.
- @ivar x: our Diffie-Hellman private key.
- @ivar e: our Diffie-Hellman public key.
- @ivar g: the Diffie-Hellman group generator.
- @ivar p: the Diffie-Hellman group prime
- @ivar instance: the SSHService object we are requesting.
- @ivar _dhMinimalGroupSize: Minimal acceptable group size advertised by the
- client in MSG_KEX_DH_GEX_REQUEST.
- @type _dhMinimalGroupSize: int
- @ivar _dhMaximalGroupSize: Maximal acceptable group size advertised by the
- client in MSG_KEX_DH_GEX_REQUEST.
- @type _dhMaximalGroupSize: int
- @ivar _dhPreferredGroupSize: Preferred group size advertised by the client
- in MSG_KEX_DH_GEX_REQUEST.
- @type _dhPreferredGroupSize: int
- """
- isClient = True
- # Recommended minimal and maximal values from RFC 4419, 3.
- _dhMinimalGroupSize = 1024
- _dhMaximalGroupSize = 8192
- # FIXME: https://twistedmatrix.com/trac/ticket/8103
- # This may need to be more dynamic; compare kexgex_client in
- # OpenSSH.
- _dhPreferredGroupSize = 2048
- def connectionMade(self):
- """
- Called when the connection is started with the server. Just sets
- up a private instance variable.
- """
- SSHTransportBase.connectionMade(self)
- self._gotNewKeys = 0
- def ssh_KEXINIT(self, packet):
- """
- Called when we receive a MSG_KEXINIT message. For a description
- of the packet, see SSHTransportBase.ssh_KEXINIT(). Additionally,
- this method sends the first key exchange packet.
- If the agreed-upon exchange is ECDH, generate a key pair for the
- corresponding curve and send the public key.
- If the agreed-upon exchange has a fixed prime/generator group,
- generate a public key and send it in a MSG_KEXDH_INIT message.
- Otherwise, ask for a 2048 bit group with a MSG_KEX_DH_GEX_REQUEST
- message.
- """
- if SSHTransportBase.ssh_KEXINIT(self, packet) is None:
- # Connection was disconnected while doing base processing.
- # Maybe no common protocols were agreed.
- return
- # Are we using ECDH?
- if _kex.isEllipticCurve(self.kexAlg):
- # Generate the keys
- self.ecPriv = self._generateECPrivateKey()
- self.ecPub = self.ecPriv.public_key()
- # DH_GEX_REQUEST_OLD is the same number we need.
- self.sendPacket(
- MSG_KEX_DH_GEX_REQUEST_OLD, NS(self._encodeECPublicKey(self.ecPub))
- )
- elif _kex.isFixedGroup(self.kexAlg):
- # We agreed on a fixed group key exchange algorithm.
- self.g, self.p = _kex.getDHGeneratorAndPrime(self.kexAlg)
- self._startEphemeralDH()
- self.sendPacket(MSG_KEXDH_INIT, self.dhSecretKeyPublicMP)
- else:
- # We agreed on a dynamic group. Tell the server what range of
- # group sizes we accept, and what size we prefer; the server
- # will then select a group.
- self.sendPacket(
- MSG_KEX_DH_GEX_REQUEST,
- struct.pack(
- "!LLL",
- self._dhMinimalGroupSize,
- self._dhPreferredGroupSize,
- self._dhMaximalGroupSize,
- ),
- )
- def _ssh_KEX_ECDH_REPLY(self, packet):
- """
- Called to handle a reply to a ECDH exchange message(KEX_ECDH_INIT).
- Like the handler for I{KEXDH_INIT}, this message type has an
- overlapping value. This method is called from C{ssh_KEX_DH_GEX_GROUP}
- if that method detects a non-group key exchange is in progress.
- Payload::
- string serverHostKey
- string server Elliptic Curve Diffie-Hellman public key
- string signature
- We verify the host key and continue if it passes verificiation.
- Otherwise raise an exception and return.
- @type packet: L{bytes}
- @param packet: The message data.
- @return: A deferred firing when key exchange is complete.
- """
- def _continue_KEX_ECDH_REPLY(ignored, hostKey, pubKey, signature):
- # Save off the host public key.
- theirECHost = hostKey
- sharedSecret = self._generateECSharedSecret(self.ecPriv, pubKey)
- h = _kex.getHashProcessor(self.kexAlg)()
- h.update(NS(self.ourVersionString))
- h.update(NS(self.otherVersionString))
- h.update(NS(self.ourKexInitPayload))
- h.update(NS(self.otherKexInitPayload))
- h.update(NS(theirECHost))
- h.update(NS(self._encodeECPublicKey(self.ecPub)))
- h.update(NS(pubKey))
- h.update(sharedSecret)
- exchangeHash = h.digest()
- if not keys.Key.fromString(theirECHost).verify(signature, exchangeHash):
- self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, b"bad signature")
- else:
- self._keySetup(sharedSecret, exchangeHash)
- # Get the host public key,
- # the raw ECDH public key bytes and the signature
- hostKey, pubKey, signature, packet = getNS(packet, 3)
- # Easier to comment this out for now than to update all of the tests.
- # fingerprint = nativeString(base64.b64encode(
- # sha256(hostKey).digest()))
- fingerprint = b":".join(
- [binascii.hexlify(ch) for ch in iterbytes(md5(hostKey).digest())]
- )
- d = self.verifyHostKey(hostKey, fingerprint)
- d.addCallback(_continue_KEX_ECDH_REPLY, hostKey, pubKey, signature)
- d.addErrback(
- lambda unused: self.sendDisconnect(
- DISCONNECT_HOST_KEY_NOT_VERIFIABLE, b"bad host key"
- )
- )
- return d
- def _ssh_KEXDH_REPLY(self, packet):
- """
- Called to handle a reply to a non-group key exchange message
- (KEXDH_INIT).
- Like the handler for I{KEXDH_INIT}, this message type has an
- overlapping value. This method is called from C{ssh_KEX_DH_GEX_GROUP}
- if that method detects a non-group key exchange is in progress.
- Payload::
- string serverHostKey
- integer f (server Diffie-Hellman public key)
- string signature
- We verify the host key by calling verifyHostKey, then continue in
- _continueKEXDH_REPLY.
- @type packet: L{bytes}
- @param packet: The message data.
- @return: A deferred firing when key exchange is complete.
- """
- pubKey, packet = getNS(packet)
- f, packet = getMP(packet)
- signature, packet = getNS(packet)
- fingerprint = b":".join(
- [binascii.hexlify(ch) for ch in iterbytes(md5(pubKey).digest())]
- )
- d = self.verifyHostKey(pubKey, fingerprint)
- d.addCallback(self._continueKEXDH_REPLY, pubKey, f, signature)
- d.addErrback(
- lambda unused: self.sendDisconnect(
- DISCONNECT_HOST_KEY_NOT_VERIFIABLE, b"bad host key"
- )
- )
- return d
- def ssh_KEX_DH_GEX_GROUP(self, packet):
- """
- This handles different messages which share an integer value.
- If the key exchange does not have a fixed prime/generator group,
- we generate a Diffie-Hellman public key and send it in a
- MSG_KEX_DH_GEX_INIT message.
- Payload::
- string g (group generator)
- string p (group prime)
- @type packet: L{bytes}
- @param packet: The message data.
- """
- if _kex.isFixedGroup(self.kexAlg):
- return self._ssh_KEXDH_REPLY(packet)
- elif _kex.isEllipticCurve(self.kexAlg):
- return self._ssh_KEX_ECDH_REPLY(packet)
- else:
- self.p, rest = getMP(packet)
- self.g, rest = getMP(rest)
- self._startEphemeralDH()
- self.sendPacket(MSG_KEX_DH_GEX_INIT, self.dhSecretKeyPublicMP)
- def _continueKEXDH_REPLY(self, ignored, pubKey, f, signature):
- """
- The host key has been verified, so we generate the keys.
- @param ignored: Ignored.
- @param pubKey: the public key blob for the server's public key.
- @type pubKey: L{str}
- @param f: the server's Diffie-Hellman public key.
- @type f: L{int}
- @param signature: the server's signature, verifying that it has the
- correct private key.
- @type signature: L{str}
- """
- serverKey = keys.Key.fromString(pubKey)
- sharedSecret = self._finishEphemeralDH(f)
- h = _kex.getHashProcessor(self.kexAlg)()
- h.update(NS(self.ourVersionString))
- h.update(NS(self.otherVersionString))
- h.update(NS(self.ourKexInitPayload))
- h.update(NS(self.otherKexInitPayload))
- h.update(NS(pubKey))
- h.update(self.dhSecretKeyPublicMP)
- h.update(MP(f))
- h.update(sharedSecret)
- exchangeHash = h.digest()
- if not serverKey.verify(signature, exchangeHash):
- self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, b"bad signature")
- return
- self._keySetup(sharedSecret, exchangeHash)
- def ssh_KEX_DH_GEX_REPLY(self, packet):
- """
- Called when we receive a MSG_KEX_DH_GEX_REPLY message. Payload::
- string server host key
- integer f (server DH public key)
- We verify the host key by calling verifyHostKey, then continue in
- _continueGEX_REPLY.
- @type packet: L{bytes}
- @param packet: The message data.
- @return: A deferred firing once key exchange is complete.
- """
- pubKey, packet = getNS(packet)
- f, packet = getMP(packet)
- signature, packet = getNS(packet)
- fingerprint = b":".join(
- [binascii.hexlify(c) for c in iterbytes(md5(pubKey).digest())]
- )
- d = self.verifyHostKey(pubKey, fingerprint)
- d.addCallback(self._continueGEX_REPLY, pubKey, f, signature)
- d.addErrback(
- lambda unused: self.sendDisconnect(
- DISCONNECT_HOST_KEY_NOT_VERIFIABLE, b"bad host key"
- )
- )
- return d
- def _continueGEX_REPLY(self, ignored, pubKey, f, signature):
- """
- The host key has been verified, so we generate the keys.
- @param ignored: Ignored.
- @param pubKey: the public key blob for the server's public key.
- @type pubKey: L{str}
- @param f: the server's Diffie-Hellman public key.
- @type f: L{int}
- @param signature: the server's signature, verifying that it has the
- correct private key.
- @type signature: L{str}
- """
- serverKey = keys.Key.fromString(pubKey)
- sharedSecret = self._finishEphemeralDH(f)
- h = _kex.getHashProcessor(self.kexAlg)()
- h.update(NS(self.ourVersionString))
- h.update(NS(self.otherVersionString))
- h.update(NS(self.ourKexInitPayload))
- h.update(NS(self.otherKexInitPayload))
- h.update(NS(pubKey))
- h.update(
- struct.pack(
- "!LLL",
- self._dhMinimalGroupSize,
- self._dhPreferredGroupSize,
- self._dhMaximalGroupSize,
- )
- )
- h.update(MP(self.p))
- h.update(MP(self.g))
- h.update(self.dhSecretKeyPublicMP)
- h.update(MP(f))
- h.update(sharedSecret)
- exchangeHash = h.digest()
- if not serverKey.verify(signature, exchangeHash):
- self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, b"bad signature")
- return
- self._keySetup(sharedSecret, exchangeHash)
- def _keySetup(self, sharedSecret, exchangeHash):
- """
- See SSHTransportBase._keySetup().
- """
- SSHTransportBase._keySetup(self, sharedSecret, exchangeHash)
- if self._gotNewKeys:
- self.ssh_NEWKEYS(b"")
- def ssh_NEWKEYS(self, packet):
- """
- Called when we receive a MSG_NEWKEYS message. No payload.
- If we've finished setting up our own keys, start using them.
- Otherwise, remember that we've received this message.
- @type packet: L{bytes}
- @param packet: The message data.
- """
- if packet != b"":
- self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, b"NEWKEYS takes no data")
- return
- if not self.nextEncryptions.encBlockSize:
- self._gotNewKeys = 1
- return
- self._newKeys()
- self.connectionSecure()
- def ssh_SERVICE_ACCEPT(self, packet):
- """
- Called when we receive a MSG_SERVICE_ACCEPT message. Payload::
- string service name
- Start the service we requested.
- @type packet: L{bytes}
- @param packet: The message data.
- """
- if packet == b"":
- self._log.info("got SERVICE_ACCEPT without payload")
- else:
- name = getNS(packet)[0]
- if name != self.instance.name:
- self.sendDisconnect(
- DISCONNECT_PROTOCOL_ERROR,
- b"received accept for service we did not request",
- )
- self.setService(self.instance)
- def requestService(self, instance):
- """
- Request that a service be run over this transport.
- @type instance: subclass of L{twisted.conch.ssh.service.SSHService}
- @param instance: The service to run.
- """
- self.sendPacket(MSG_SERVICE_REQUEST, NS(instance.name))
- self.instance = instance
- # Client methods
- def verifyHostKey(self, hostKey, fingerprint):
- """
- Returns a Deferred that gets a callback if it is a valid key, or
- an errback if not.
- @type hostKey: L{bytes}
- @param hostKey: The host key to verify.
- @type fingerprint: L{bytes}
- @param fingerprint: The fingerprint of the key.
- @return: A deferred firing with C{True} if the key is valid.
- """
- return defer.fail(NotImplementedError())
- def connectionSecure(self):
- """
- Called when the encryption has been set up. Generally,
- requestService() is called to run another service over the transport.
- """
- raise NotImplementedError()
- class _NullEncryptionContext:
- """
- An encryption context that does not actually encrypt anything.
- """
- def update(self, data):
- """
- 'Encrypt' new data by doing nothing.
- @type data: L{bytes}
- @param data: The data to 'encrypt'.
- @rtype: L{bytes}
- @return: The 'encrypted' data.
- """
- return data
- class _DummyAlgorithm:
- """
- An encryption algorithm that does not actually encrypt anything.
- """
- block_size = 64
- class _DummyCipher:
- """
- A cipher for the none encryption method.
- @ivar block_size: the block size of the encryption. In the case of the
- none cipher, this is 8 bytes.
- """
- algorithm = _DummyAlgorithm()
- def encryptor(self):
- """
- Construct a noop encryptor.
- @return: The encryptor.
- """
- return _NullEncryptionContext()
- def decryptor(self):
- """
- Construct a noop decryptor.
- @return: The decryptor.
- """
- return _NullEncryptionContext()
- DH_GENERATOR, DH_PRIME = _kex.getDHGeneratorAndPrime(b"diffie-hellman-group14-sha1")
- MSG_DISCONNECT = 1
- MSG_IGNORE = 2
- MSG_UNIMPLEMENTED = 3
- MSG_DEBUG = 4
- MSG_SERVICE_REQUEST = 5
- MSG_SERVICE_ACCEPT = 6
- MSG_EXT_INFO = 7
- MSG_KEXINIT = 20
- MSG_NEWKEYS = 21
- MSG_KEXDH_INIT = 30
- MSG_KEXDH_REPLY = 31
- MSG_KEX_DH_GEX_REQUEST_OLD = 30
- MSG_KEX_DH_GEX_REQUEST = 34
- MSG_KEX_DH_GEX_GROUP = 31
- MSG_KEX_DH_GEX_INIT = 32
- MSG_KEX_DH_GEX_REPLY = 33
- DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1
- DISCONNECT_PROTOCOL_ERROR = 2
- DISCONNECT_KEY_EXCHANGE_FAILED = 3
- DISCONNECT_RESERVED = 4
- DISCONNECT_MAC_ERROR = 5
- DISCONNECT_COMPRESSION_ERROR = 6
- DISCONNECT_SERVICE_NOT_AVAILABLE = 7
- DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED = 8
- DISCONNECT_HOST_KEY_NOT_VERIFIABLE = 9
- DISCONNECT_CONNECTION_LOST = 10
- DISCONNECT_BY_APPLICATION = 11
- DISCONNECT_TOO_MANY_CONNECTIONS = 12
- DISCONNECT_AUTH_CANCELLED_BY_USER = 13
- DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14
- DISCONNECT_ILLEGAL_USER_NAME = 15
- messages = {}
- for name, value in list(globals().items()):
- # Avoid legacy messages which overlap with never ones
- if name.startswith("MSG_") and not name.startswith("MSG_KEXDH_"):
- messages[value] = name
- # Check for regressions (#5352)
- if "MSG_KEXDH_INIT" in messages or "MSG_KEXDH_REPLY" in messages:
- raise RuntimeError("legacy SSH mnemonics should not end up in messages dict")
|