예제 #1
0
    def test_mismatchedHostKey(self):
        """
        If the SSH public key presented by the SSH server does not match the
        previously remembered key, as reported by the L{KnownHostsFile}
        instance use to construct the endpoint, for that server, the
        L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with
        a L{Failure} wrapping L{HostKeyChanged}.
        """
        differentKey = Key.fromString(privateDSA_openssh).public()
        knownHosts = KnownHostsFile(self.mktemp())
        knownHosts.addHostKey(self.serverAddress.host, differentKey)
        knownHosts.addHostKey(self.hostname, differentKey)

        # The UI may answer true to any questions asked of it; they should
        # make no difference, since a *mismatched* key is not even optionally
        # allowed to complete a connection.
        ui = FixedResponseUI(True)

        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", b"dummy user",
            self.hostname, self.port, password=b"dummy password",
            knownHosts=knownHosts, ui=ui)

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        f = self.failureResultOf(connected)
        f.trap(HostKeyChanged)
예제 #2
0
 def test_iterentriesUnsaved(self):
     """
     If the save path for a L{KnownHostsFile} does not exist,
     L{KnownHostsFile.iterentries} still returns added but unsaved entries.
     """
     hostsFile = KnownHostsFile(FilePath(self.mktemp()))
     hostsFile.addHostKey("www.example.com", Key.fromString(sampleKey))
     self.assertEqual(1, len(list(hostsFile.iterentries())))
예제 #3
0
 def test_iterentriesUnsaved(self):
     """
     If the save path for a L{KnownHostsFile} does not exist,
     L{KnownHostsFile.iterentries} still returns added but unsaved entries.
     """
     hostsFile = KnownHostsFile(FilePath(self.mktemp()))
     hostsFile.addHostKey("www.example.com", Key.fromString(sampleKey))
     self.assertEqual(1, len(list(hostsFile.iterentries())))
예제 #4
0
    def test_saveResetsClobberState(self):
        """
        After L{KnownHostsFile.save} is used once with an instance initialized
        by the default initializer, contents of the save path are respected and
        preserved.
        """
        hostsFile = KnownHostsFile(self.pathWithContent(sampleHashedLine))
        preSave = hostsFile.addHostKey("www.example.com",
                                       Key.fromString(otherSampleKey))
        hostsFile.save()
        postSave = hostsFile.addHostKey("another.example.com",
                                        Key.fromString(thirdSampleKey))
        hostsFile.save()

        self.assertEqual([preSave, postSave], list(hostsFile.iterentries()))
예제 #5
0
    def test_saveResetsClobberState(self):
        """
        After L{KnownHostsFile.save} is used once with an instance initialized
        by the default initializer, contents of the save path are respected and
        preserved.
        """
        hostsFile = KnownHostsFile(self.pathWithContent(sampleHashedLine))
        preSave = hostsFile.addHostKey(
            "www.example.com", Key.fromString(otherSampleKey))
        hostsFile.save()
        postSave = hostsFile.addHostKey(
            "another.example.com", Key.fromString(thirdSampleKey))
        hostsFile.save()

        self.assertEqual([preSave, postSave], list(hostsFile.iterentries()))
예제 #6
0
 def setUp(self):
     """
     Patch 'open' in verifyHostKey.
     """
     self.fakeFile = FakeFile()
     self.patch(default, "_open", self.patchedOpen)
     self.hostsOption = self.mktemp()
     knownHostsFile = KnownHostsFile(FilePath(self.hostsOption))
     knownHostsFile.addHostKey("exists.example.com", Key.fromString(sampleKey))
     knownHostsFile.addHostKey("4.3.2.1", Key.fromString(sampleKey))
     knownHostsFile.save()
     self.fakeTransport = FakeObject()
     self.fakeTransport.factory = FakeObject()
     self.options = self.fakeTransport.factory.options = {
         'host': "exists.example.com",
         'known-hosts': self.hostsOption
         }
예제 #7
0
    def setUp(self):
        """
        Configure an SSH server with password authentication enabled for a
        well-known (to the tests) account.
        """
        SSHCommandClientEndpointTestsMixin.setUp(self)

        knownHosts = KnownHostsFile(FilePath(self.mktemp()))
        knownHosts.addHostKey(
            self.hostname, self.factory.publicKeys['ssh-rsa'])
        knownHosts.addHostKey(
            self.serverAddress.host, self.factory.publicKeys['ssh-rsa'])

        self.endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port,
            password=self.password, knownHosts=knownHosts,
            ui=FixedResponseUI(False))
예제 #8
0
 def setUp(self):
     """
     Patch 'open' in verifyHostKey.
     """
     self.fakeFile = FakeFile()
     self.patch(default, "_open", self.patchedOpen)
     self.hostsOption = self.mktemp()
     knownHostsFile = KnownHostsFile(FilePath(self.hostsOption))
     knownHostsFile.addHostKey("exists.example.com",
                               Key.fromString(sampleKey))
     knownHostsFile.addHostKey("4.3.2.1", Key.fromString(sampleKey))
     knownHostsFile.save()
     self.fakeTransport = FakeObject()
     self.fakeTransport.factory = FakeObject()
     self.options = self.fakeTransport.factory.options = {
         'host': "exists.example.com",
         'known-hosts': self.hostsOption
     }
예제 #9
0
    def test_readExisting(self):
        """
        Existing entries in the I{known_hosts} file are reflected by the
        L{KnownHostsFile} created by L{_NewConnectionHelper} when none is
        supplied to it.
        """
        key = CommandFactory().publicKeys['ssh-rsa']
        path = FilePath(self.mktemp())
        knownHosts = KnownHostsFile(path)
        knownHosts.addHostKey("127.0.0.1", key)
        knownHosts.save()

        msg("Created known_hosts file at %r" % (path.path,))

        # Unexpand ${HOME} to make sure ~ syntax is respected.
        home = os.path.expanduser("~/")
        default = path.path.replace(home, "~/")
        self.patch(_NewConnectionHelper, "_KNOWN_HOSTS", default)
        msg("Patched _KNOWN_HOSTS with %r" % (default,))

        loaded = _NewConnectionHelper._knownHosts()
        self.assertTrue(loaded.hasHostKey("127.0.0.1", key))
예제 #10
0
    def test_readExisting(self):
        """
        Existing entries in the I{known_hosts} file are reflected by the
        L{KnownHostsFile} created by L{_NewConnectionHelper} when none is
        supplied to it.
        """
        key = CommandFactory().publicKeys['ssh-rsa']
        path = FilePath(self.mktemp())
        knownHosts = KnownHostsFile(path)
        knownHosts.addHostKey("127.0.0.1", key)
        knownHosts.save()

        msg("Created known_hosts file at %r" % (path.path, ))

        # Unexpand ${HOME} to make sure ~ syntax is respected.
        home = os.path.expanduser("~/")
        default = path.path.replace(home, "~/")
        self.patch(_NewConnectionHelper, "_KNOWN_HOSTS", default)
        msg("Patched _KNOWN_HOSTS with %r" % (default, ))

        loaded = _NewConnectionHelper._knownHosts()
        self.assertTrue(loaded.hasHostKey("127.0.0.1", key))
예제 #11
0
    def setUp(self):
        """
        Configure an SSH server with password authentication enabled for a
        well-known (to the tests) account.
        """
        SSHCommandClientEndpointTestsMixin.setUp(self)

        knownHosts = KnownHostsFile(FilePath(self.mktemp()))
        knownHosts.addHostKey(self.hostname,
                              self.factory.publicKeys['ssh-rsa'])
        knownHosts.addHostKey(self.serverAddress.host,
                              self.factory.publicKeys['ssh-rsa'])

        self.endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            self.user,
            self.hostname,
            self.port,
            password=self.password,
            knownHosts=knownHosts,
            ui=FixedResponseUI(False))
예제 #12
0
    def test_mismatchedHostKey(self):
        """
        If the SSH public key presented by the SSH server does not match the
        previously remembered key, as reported by the L{KnownHostsFile}
        instance use to construct the endpoint, for that server, the
        L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with
        a L{Failure} wrapping L{HostKeyChanged}.
        """
        differentKey = Key.fromString(privateDSA_openssh).public()
        knownHosts = KnownHostsFile(self.mktemp())
        knownHosts.addHostKey(self.serverAddress.host, differentKey)
        knownHosts.addHostKey(self.hostname, differentKey)

        # The UI may answer true to any questions asked of it; they should
        # make no difference, since a *mismatched* key is not even optionally
        # allowed to complete a connection.
        ui = FixedResponseUI(True)

        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            b"dummy user",
            self.hostname,
            self.port,
            password=b"dummy password",
            knownHosts=knownHosts,
            ui=ui)

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        f = self.failureResultOf(connected)
        f.trap(HostKeyChanged)
예제 #13
0
    def test_savingAvoidsDuplication(self):
        """
        L{KnownHostsFile.save} only writes new entries to the save path, not
        entries which were added and already written by a previous call to
        C{save}.
        """
        path = FilePath(self.mktemp())
        knownHosts = KnownHostsFile(path)
        entry = knownHosts.addHostKey("some.example.com",
                                      Key.fromString(sampleKey))
        knownHosts.save()
        knownHosts.save()

        knownHosts = KnownHostsFile.fromPath(path)
        self.assertEqual([entry], list(knownHosts.iterentries()))
예제 #14
0
 def test_defaultInitializerClobbersExisting(self):
     """
     After using the default initializer for L{KnownHostsFile}, the first use
     of L{KnownHostsFile.save} overwrites any existing contents in the save
     path.
     """
     path = self.pathWithContent(sampleHashedLine)
     hostsFile = KnownHostsFile(path)
     entry = hostsFile.addHostKey("www.example.com",
                                  Key.fromString(otherSampleKey))
     hostsFile.save()
     # Check KnownHostsFile to see what it thinks the state is
     self.assertEqual([entry], list(hostsFile.iterentries()))
     # And also directly check the underlying file itself
     self.assertEqual(entry.toString() + "\n", path.getContent())
예제 #15
0
    def test_savingAvoidsDuplication(self):
        """
        L{KnownHostsFile.save} only writes new entries to the save path, not
        entries which were added and already written by a previous call to
        C{save}.
        """
        path = FilePath(self.mktemp())
        knownHosts = KnownHostsFile(path)
        entry = knownHosts.addHostKey(
            "some.example.com", Key.fromString(sampleKey))
        knownHosts.save()
        knownHosts.save()

        knownHosts = KnownHostsFile.fromPath(path)
        self.assertEqual([entry], list(knownHosts.iterentries()))
예제 #16
0
 def test_defaultInitializerClobbersExisting(self):
     """
     After using the default initializer for L{KnownHostsFile}, the first use
     of L{KnownHostsFile.save} overwrites any existing contents in the save
     path.
     """
     path = self.pathWithContent(sampleHashedLine)
     hostsFile = KnownHostsFile(path)
     entry = hostsFile.addHostKey(
         "www.example.com", Key.fromString(otherSampleKey))
     hostsFile.save()
     # Check KnownHostsFile to see what it thinks the state is
     self.assertEqual([entry], list(hostsFile.iterentries()))
     # And also directly check the underlying file itself
     self.assertEqual(entry.toString() + "\n", path.getContent())
예제 #17
0
 def test_unsavedEntryHasKeyMismatch(self):
     """
     L{KnownHostsFile.hasHostKey} raises L{HostKeyChanged} if the host key is
     present in memory (but not yet saved), but different from the expected
     one.  The resulting exception has a C{offendingEntry} indicating the
     given entry, but no filename or line number information (reflecting the
     fact that the entry exists only in memory).
     """
     hostsFile = KnownHostsFile(FilePath(self.mktemp()))
     entry = hostsFile.addHostKey("www.example.com",
                                  Key.fromString(otherSampleKey))
     exception = self.assertRaises(HostKeyChanged, hostsFile.hasHostKey,
                                   "www.example.com",
                                   Key.fromString(thirdSampleKey))
     self.assertEqual(exception.offendingEntry, entry)
     self.assertEqual(exception.lineno, None)
     self.assertEqual(exception.path, None)
예제 #18
0
 def test_unsavedEntryHasKeyMismatch(self):
     """
     L{KnownHostsFile.hasHostKey} raises L{HostKeyChanged} if the host key is
     present in memory (but not yet saved), but different from the expected
     one.  The resulting exception has a C{offendingEntry} indicating the
     given entry, but no filename or line number information (reflecting the
     fact that the entry exists only in memory).
     """
     hostsFile = KnownHostsFile(FilePath(self.mktemp()))
     entry = hostsFile.addHostKey(
         "www.example.com", Key.fromString(otherSampleKey))
     exception = self.assertRaises(
         HostKeyChanged, hostsFile.hasHostKey,
         "www.example.com", Key.fromString(thirdSampleKey))
     self.assertEqual(exception.offendingEntry, entry)
     self.assertEqual(exception.lineno, None)
     self.assertEqual(exception.path, None)
예제 #19
0
 def setUp(self):
     """
     Patch 'open' in verifyHostKey.
     """
     self.fakeFile = FakeFile()
     self.patch(default, "_open", self.patchedOpen)
     self.hostsOption = self.mktemp()
     self.hashedEntries = {}
     knownHostsFile = KnownHostsFile(FilePath(self.hostsOption))
     for host in (b"exists.example.com", b"4.3.2.1"):
         entry = knownHostsFile.addHostKey(host, Key.fromString(sampleKey))
         self.hashedEntries[host] = entry
     knownHostsFile.save()
     self.fakeTransport = FakeObject()
     self.fakeTransport.factory = FakeObject()
     self.options = self.fakeTransport.factory.options = {
         'host': b"exists.example.com",
         'known-hosts': self.hostsOption
     }
예제 #20
0
 def setUp(self):
     """
     Patch 'open' in verifyHostKey.
     """
     self.fakeFile = FakeFile()
     self.patch(default, "_open", self.patchedOpen)
     self.hostsOption = self.mktemp()
     self.hashedEntries = {}
     knownHostsFile = KnownHostsFile(FilePath(self.hostsOption))
     for host in (b"exists.example.com", b"4.3.2.1"):
         entry = knownHostsFile.addHostKey(host, Key.fromString(sampleKey))
         self.hashedEntries[host] = entry
     knownHostsFile.save()
     self.fakeTransport = FakeObject()
     self.fakeTransport.factory = FakeObject()
     self.options = self.fakeTransport.factory.options = {
         'host': b"exists.example.com",
         'known-hosts': self.hostsOption
         }
예제 #21
0
class NewConnectionTests(TestCase, SSHCommandClientEndpointTestsMixin):
    """
    Tests for L{SSHCommandClientEndpoint} when using the C{newConnection}
    constructor.
    """
    def setUp(self):
        """
        Configure an SSH server with password authentication enabled for a
        well-known (to the tests) account.
        """
        SSHCommandClientEndpointTestsMixin.setUp(self)
        # Make the server's host key available to be verified by the client.
        self.hostKeyPath = FilePath(self.mktemp())
        self.knownHosts = KnownHostsFile(self.hostKeyPath)
        self.knownHosts.addHostKey(
            self.hostname, self.factory.publicKeys['ssh-rsa'])
        self.knownHosts.addHostKey(
            self.serverAddress.host, self.factory.publicKeys['ssh-rsa'])
        self.knownHosts.save()


    def create(self):
        """
        Create and return a new L{SSHCommandClientEndpoint} using the
        C{newConnection} constructor.
        """
        return SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port,
            password=self.password, knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))


    def finishConnection(self):
        """
        Establish the first attempted TCP connection using the SSH server which
        C{self.factory} can create.
        """
        return self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])


    def assertClientTransportState(self, client, immediateClose):
        """
        Assert that the transport for the given protocol has been disconnected.
        L{SSHCommandClientEndpoint.newConnection} creates a new dedicated SSH
        connection and cleans it up after the command exits.
        """
        # Nothing useful can be done with the connection at this point, so the
        # endpoint should close it.
        if immediateClose:
            self.assertTrue(client.transport.aborted)
        else:
            self.assertTrue(client.transport.disconnecting)


    def test_destination(self):
        """
        L{SSHCommandClientEndpoint} uses the L{IReactorTCP} passed to it to
        attempt a connection to the host/port address also passed to it.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port,
            password=self.password, knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))
        factory = Factory()
        factory.protocol = Protocol
        endpoint.connect(factory)

        host, port, factory, timeout, bindAddress = self.reactor.tcpClients[0]
        self.assertEqual(self.hostname, host)
        self.assertEqual(self.port, port)
        self.assertEqual(1, len(self.reactor.tcpClients))


    def test_connectionFailed(self):
        """
        If a connection cannot be established, the L{Deferred} returned by
        L{SSHCommandClientEndpoint.connect} fires with a L{Failure}
        representing the reason for the connection setup failure.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", b"dummy user",
            self.hostname, self.port, knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))
        factory = Factory()
        factory.protocol = Protocol
        d = endpoint.connect(factory)

        factory = self.reactor.tcpClients[0][2]
        factory.clientConnectionFailed(None, Failure(ConnectionRefusedError()))

        self.failureResultOf(d).trap(ConnectionRefusedError)


    def test_userRejectedHostKey(self):
        """
        If the L{KnownHostsFile} instance used to construct
        L{SSHCommandClientEndpoint} rejects the SSH public key presented by the
        server, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect}
        fires with a L{Failure} wrapping L{UserRejectedKey}.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", b"dummy user",
            self.hostname, self.port, knownHosts=KnownHostsFile(self.mktemp()),
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        f = self.failureResultOf(connected)
        f.trap(UserRejectedKey)


    def test_mismatchedHostKey(self):
        """
        If the SSH public key presented by the SSH server does not match the
        previously remembered key, as reported by the L{KnownHostsFile}
        instance use to construct the endpoint, for that server, the
        L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with
        a L{Failure} wrapping L{HostKeyChanged}.
        """
        differentKey = Key.fromString(privateDSA_openssh).public()
        knownHosts = KnownHostsFile(self.mktemp())
        knownHosts.addHostKey(self.serverAddress.host, differentKey)
        knownHosts.addHostKey(self.hostname, differentKey)

        # The UI may answer true to any questions asked of it; they should
        # make no difference, since a *mismatched* key is not even optionally
        # allowed to complete a connection.
        ui = FixedResponseUI(True)

        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", b"dummy user",
            self.hostname, self.port, password=b"dummy password",
            knownHosts=knownHosts, ui=ui)

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        f = self.failureResultOf(connected)
        f.trap(HostKeyChanged)


    def test_connectionClosedBeforeSecure(self):
        """
        If the connection closes at any point before the SSH transport layer
        has finished key exchange (ie, gotten to the point where we may attempt
        to authenticate), the L{Deferred} returned by
        L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping
        the reason for the lost connection.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", b"dummy user",
            self.hostname, self.port, knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        d = endpoint.connect(factory)

        transport = StringTransport()
        factory = self.reactor.tcpClients[0][2]
        client = factory.buildProtocol(None)
        client.makeConnection(transport)

        client.connectionLost(Failure(ConnectionDone()))
        self.failureResultOf(d).trap(ConnectionDone)


    def test_connectionCancelledBeforeSecure(self):
        """
        If the connection is cancelled before the SSH transport layer has
        finished key exchange (ie, gotten to the point where we may attempt to
        authenticate), the L{Deferred} returned by
        L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping
        L{CancelledError} and the connection is aborted.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", b"dummy user",
            self.hostname, self.port, knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        d = endpoint.connect(factory)

        transport = AbortableFakeTransport(None, isServer=False)
        factory = self.reactor.tcpClients[0][2]
        client = factory.buildProtocol(None)
        client.makeConnection(transport)
        d.cancel()

        self.failureResultOf(d).trap(CancelledError)
        self.assertTrue(transport.aborted)
        # Make sure the connection closing doesn't result in unexpected
        # behavior when due to cancellation:
        client.connectionLost(Failure(ConnectionDone()))


    def test_connectionCancelledBeforeConnected(self):
        """
        If the connection is cancelled before it finishes connecting, the
        connection attempt is stopped.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", b"dummy user",
            self.hostname, self.port, knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        d = endpoint.connect(factory)
        d.cancel()
        self.failureResultOf(d).trap(ConnectingCancelledError)
        self.assertTrue(self.reactor.connectors[0].stoppedConnecting)


    def test_passwordAuthenticationFailure(self):
        """
        If the SSH server rejects the password presented during authentication,
        the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires
        with a L{Failure} wrapping L{AuthenticationFailed}.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", b"dummy user",
            self.hostname, self.port,  password=b"dummy password",
            knownHosts=self.knownHosts, ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        # For security, the server delays password authentication failure
        # response.  Advance the simulation clock so the client sees the
        # failure.
        self.reactor.advance(server.service.passwordDelay)

        # Let the failure response traverse the "network"
        pump.flush()

        f = self.failureResultOf(connected)
        f.trap(AuthenticationFailed)
        # XXX Should assert something specific about the arguments of the
        # exception

        self.assertClientTransportState(client, False)


    def setupKeyChecker(self, portal, users):
        """
        Create an L{ISSHPrivateKey} checker which recognizes C{users} and add it
        to C{portal}.

        @param portal: A L{Portal} to which to add the checker.
        @type portal: L{Portal}

        @param users: The users and their keys the checker will recognize.  Keys
            are byte strings giving user names.  Values are byte strings giving
            OpenSSH-formatted private keys.
        @type users: C{dict}
        """
        credentials = {}
        for username, keyString in users.iteritems():
            goodKey = Key.fromString(keyString)
            authorizedKeys = FilePath(self.mktemp())
            authorizedKeys.setContent(goodKey.public().toString("OPENSSH"))
            credentials[username] = [authorizedKeys]
        checker = MemorySSHPublicKeyDatabase(credentials)
        portal.registerChecker(checker)


    def test_publicKeyAuthenticationFailure(self):
        """
        If the SSH server rejects the key pair presented during authentication,
        the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires
        with a L{Failure} wrapping L{AuthenticationFailed}.
        """
        badKey = Key.fromString(privateRSA_openssh)
        self.setupKeyChecker(self.portal, {self.user: privateDSA_openssh})

        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", self.user,
            self.hostname, self.port, keys=[badKey],
            knownHosts=self.knownHosts, ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        f = self.failureResultOf(connected)
        f.trap(AuthenticationFailed)
        # XXX Should assert something specific about the arguments of the
        # exception

        # Nothing useful can be done with the connection at this point, so the
        # endpoint should close it.
        self.assertTrue(client.transport.disconnecting)


    def test_authenticationFallback(self):
        """
        If the SSH server does not accept any of the specified SSH keys, the
        specified password is tried.
        """
        badKey = Key.fromString(privateRSA_openssh)
        self.setupKeyChecker(self.portal, {self.user: privateDSA_openssh})

        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port,
            keys=[badKey], password=self.password, knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        # Exercising fallback requires a failed authentication attempt.  Allow
        # one.
        self.factory.attemptsBeforeDisconnect += 1

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        pump.pump()

        # The server logs the channel open failure - this is expected.
        errors = self.flushLoggedErrors(ConchError)
        self.assertIn(
            'unknown channel', (errors[0].value.data, errors[0].value.value))
        self.assertEqual(1, len(errors))

        # Now deal with the results on the endpoint side.
        f = self.failureResultOf(connected)
        f.trap(ConchError)
        self.assertEqual('unknown channel', f.value.value)

        # Nothing useful can be done with the connection at this point, so the
        # endpoint should close it.
        self.assertTrue(client.transport.disconnecting)


    def test_publicKeyAuthentication(self):
        """
        If L{SSHCommandClientEndpoint} is initialized with any private keys, it
        will try to use them to authenticate with the SSH server.
        """
        key = Key.fromString(privateDSA_openssh)
        self.setupKeyChecker(self.portal, {self.user: privateDSA_openssh})

        self.realm.channelLookup[b'session'] = WorkingExecSession
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port,
            keys=[key], knownHosts=self.knownHosts, ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        protocol = self.successResultOf(connected)
        self.assertNotIdentical(None, protocol.transport)


    def test_agentAuthentication(self):
        """
        If L{SSHCommandClientEndpoint} is initialized with an
        L{SSHAgentClient}, the agent is used to authenticate with the SSH
        server.
        """
        key = Key.fromString(privateRSA_openssh)
        agentServer = SSHAgentServer()
        agentServer.factory = Factory()
        agentServer.factory.keys = {key.blob(): (key, "")}

        self.setupKeyChecker(self.portal, {self.user: privateRSA_openssh})

        agentEndpoint = SingleUseMemoryEndpoint(agentServer)
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port,
            knownHosts=self.knownHosts, ui=FixedResponseUI(False),
            agentEndpoint=agentEndpoint)

        self.realm.channelLookup[b'session'] = WorkingExecSession

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        # Let the agent client talk with the agent server and the ssh client
        # talk with the ssh server.
        for i in range(14):
            agentEndpoint.pump.pump()
            pump.pump()

        protocol = self.successResultOf(connected)
        self.assertNotIdentical(None, protocol.transport)


    def test_loseConnection(self):
        """
        The transport connected to the protocol has a C{loseConnection} method
        which causes the channel in which the command is running to close and
        the overall connection to be closed.
        """
        self.realm.channelLookup[b'session'] = WorkingExecSession
        endpoint = self.create()

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.finishConnection()

        protocol = self.successResultOf(connected)
        closed = self.record(server, protocol, 'closed', noArgs=True)
        protocol.transport.loseConnection()
        pump.pump()
        self.assertEqual([None], closed)

        # Let the last bit of network traffic flow.  This lets the server's
        # close acknowledgement through, at which point the client can close
        # the overall SSH connection.
        pump.pump()

        # Nothing useful can be done with the connection at this point, so the
        # endpoint should close it.
        self.assertTrue(client.transport.disconnecting)
예제 #22
0
class NewConnectionTests(TestCase, SSHCommandClientEndpointTestsMixin):
    """
    Tests for L{SSHCommandClientEndpoint} when using the C{newConnection}
    constructor.
    """
    def setUp(self):
        """
        Configure an SSH server with password authentication enabled for a
        well-known (to the tests) account.
        """
        SSHCommandClientEndpointTestsMixin.setUp(self)
        # Make the server's host key available to be verified by the client.
        self.hostKeyPath = FilePath(self.mktemp())
        self.knownHosts = KnownHostsFile(self.hostKeyPath)
        self.knownHosts.addHostKey(self.hostname,
                                   self.factory.publicKeys['ssh-rsa'])
        self.knownHosts.addHostKey(self.serverAddress.host,
                                   self.factory.publicKeys['ssh-rsa'])
        self.knownHosts.save()

    def create(self):
        """
        Create and return a new L{SSHCommandClientEndpoint} using the
        C{newConnection} constructor.
        """
        return SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            self.user,
            self.hostname,
            self.port,
            password=self.password,
            knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))

    def finishConnection(self):
        """
        Establish the first attempted TCP connection using the SSH server which
        C{self.factory} can create.
        """
        return self.connectedServerAndClient(self.factory,
                                             self.reactor.tcpClients[0][2])

    def assertClientTransportState(self, client, immediateClose):
        """
        Assert that the transport for the given protocol has been disconnected.
        L{SSHCommandClientEndpoint.newConnection} creates a new dedicated SSH
        connection and cleans it up after the command exits.
        """
        # Nothing useful can be done with the connection at this point, so the
        # endpoint should close it.
        if immediateClose:
            self.assertTrue(client.transport.aborted)
        else:
            self.assertTrue(client.transport.disconnecting)

    def test_destination(self):
        """
        L{SSHCommandClientEndpoint} uses the L{IReactorTCP} passed to it to
        attempt a connection to the host/port address also passed to it.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            self.user,
            self.hostname,
            self.port,
            password=self.password,
            knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))
        factory = Factory()
        factory.protocol = Protocol
        endpoint.connect(factory)

        host, port, factory, timeout, bindAddress = self.reactor.tcpClients[0]
        self.assertEqual(self.hostname, host)
        self.assertEqual(self.port, port)
        self.assertEqual(1, len(self.reactor.tcpClients))

    def test_connectionFailed(self):
        """
        If a connection cannot be established, the L{Deferred} returned by
        L{SSHCommandClientEndpoint.connect} fires with a L{Failure}
        representing the reason for the connection setup failure.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            b"dummy user",
            self.hostname,
            self.port,
            knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))
        factory = Factory()
        factory.protocol = Protocol
        d = endpoint.connect(factory)

        factory = self.reactor.tcpClients[0][2]
        factory.clientConnectionFailed(None, Failure(ConnectionRefusedError()))

        self.failureResultOf(d).trap(ConnectionRefusedError)

    def test_userRejectedHostKey(self):
        """
        If the L{KnownHostsFile} instance used to construct
        L{SSHCommandClientEndpoint} rejects the SSH public key presented by the
        server, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect}
        fires with a L{Failure} wrapping L{UserRejectedKey}.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            b"dummy user",
            self.hostname,
            self.port,
            knownHosts=KnownHostsFile(self.mktemp()),
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        f = self.failureResultOf(connected)
        f.trap(UserRejectedKey)

    def test_mismatchedHostKey(self):
        """
        If the SSH public key presented by the SSH server does not match the
        previously remembered key, as reported by the L{KnownHostsFile}
        instance use to construct the endpoint, for that server, the
        L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with
        a L{Failure} wrapping L{HostKeyChanged}.
        """
        differentKey = Key.fromString(privateDSA_openssh).public()
        knownHosts = KnownHostsFile(self.mktemp())
        knownHosts.addHostKey(self.serverAddress.host, differentKey)
        knownHosts.addHostKey(self.hostname, differentKey)

        # The UI may answer true to any questions asked of it; they should
        # make no difference, since a *mismatched* key is not even optionally
        # allowed to complete a connection.
        ui = FixedResponseUI(True)

        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            b"dummy user",
            self.hostname,
            self.port,
            password=b"dummy password",
            knownHosts=knownHosts,
            ui=ui)

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        f = self.failureResultOf(connected)
        f.trap(HostKeyChanged)

    def test_connectionClosedBeforeSecure(self):
        """
        If the connection closes at any point before the SSH transport layer
        has finished key exchange (ie, gotten to the point where we may attempt
        to authenticate), the L{Deferred} returned by
        L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping
        the reason for the lost connection.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            b"dummy user",
            self.hostname,
            self.port,
            knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        d = endpoint.connect(factory)

        transport = StringTransport()
        factory = self.reactor.tcpClients[0][2]
        client = factory.buildProtocol(None)
        client.makeConnection(transport)

        client.connectionLost(Failure(ConnectionDone()))
        self.failureResultOf(d).trap(ConnectionDone)

    def test_connectionCancelledBeforeSecure(self):
        """
        If the connection is cancelled before the SSH transport layer has
        finished key exchange (ie, gotten to the point where we may attempt to
        authenticate), the L{Deferred} returned by
        L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping
        L{CancelledError} and the connection is aborted.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            b"dummy user",
            self.hostname,
            self.port,
            knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        d = endpoint.connect(factory)

        transport = AbortableFakeTransport(None, isServer=False)
        factory = self.reactor.tcpClients[0][2]
        client = factory.buildProtocol(None)
        client.makeConnection(transport)
        d.cancel()

        self.failureResultOf(d).trap(CancelledError)
        self.assertTrue(transport.aborted)
        # Make sure the connection closing doesn't result in unexpected
        # behavior when due to cancellation:
        client.connectionLost(Failure(ConnectionDone()))

    def test_connectionCancelledBeforeConnected(self):
        """
        If the connection is cancelled before it finishes connecting, the
        connection attempt is stopped.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            b"dummy user",
            self.hostname,
            self.port,
            knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        d = endpoint.connect(factory)
        d.cancel()
        self.failureResultOf(d).trap(ConnectingCancelledError)
        self.assertTrue(self.reactor.connectors[0].stoppedConnecting)

    def test_passwordAuthenticationFailure(self):
        """
        If the SSH server rejects the password presented during authentication,
        the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires
        with a L{Failure} wrapping L{AuthenticationFailed}.
        """
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            b"dummy user",
            self.hostname,
            self.port,
            password=b"dummy password",
            knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        # For security, the server delays password authentication failure
        # response.  Advance the simulation clock so the client sees the
        # failure.
        self.reactor.advance(server.service.passwordDelay)

        # Let the failure response traverse the "network"
        pump.flush()

        f = self.failureResultOf(connected)
        f.trap(AuthenticationFailed)
        # XXX Should assert something specific about the arguments of the
        # exception

        self.assertClientTransportState(client, False)

    def setupKeyChecker(self, portal, users):
        """
        Create an L{ISSHPrivateKey} checker which recognizes C{users} and add it
        to C{portal}.

        @param portal: A L{Portal} to which to add the checker.
        @type portal: L{Portal}

        @param users: The users and their keys the checker will recognize.  Keys
            are byte strings giving user names.  Values are byte strings giving
            OpenSSH-formatted private keys.
        @type users: C{dict}
        """
        credentials = {}
        for username, keyString in users.iteritems():
            goodKey = Key.fromString(keyString)
            authorizedKeys = FilePath(self.mktemp())
            authorizedKeys.setContent(goodKey.public().toString("OPENSSH"))
            credentials[username] = [authorizedKeys]
        checker = MemorySSHPublicKeyDatabase(credentials)
        portal.registerChecker(checker)

    def test_publicKeyAuthenticationFailure(self):
        """
        If the SSH server rejects the key pair presented during authentication,
        the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires
        with a L{Failure} wrapping L{AuthenticationFailed}.
        """
        badKey = Key.fromString(privateRSA_openssh)
        self.setupKeyChecker(self.portal, {self.user: privateDSA_openssh})

        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            self.user,
            self.hostname,
            self.port,
            keys=[badKey],
            knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        f = self.failureResultOf(connected)
        f.trap(AuthenticationFailed)
        # XXX Should assert something specific about the arguments of the
        # exception

        # Nothing useful can be done with the connection at this point, so the
        # endpoint should close it.
        self.assertTrue(client.transport.disconnecting)

    def test_authenticationFallback(self):
        """
        If the SSH server does not accept any of the specified SSH keys, the
        specified password is tried.
        """
        badKey = Key.fromString(privateRSA_openssh)
        self.setupKeyChecker(self.portal, {self.user: privateDSA_openssh})

        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            self.user,
            self.hostname,
            self.port,
            keys=[badKey],
            password=self.password,
            knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        # Exercising fallback requires a failed authentication attempt.  Allow
        # one.
        self.factory.attemptsBeforeDisconnect += 1

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        pump.pump()

        # The server logs the channel open failure - this is expected.
        errors = self.flushLoggedErrors(ConchError)
        self.assertIn('unknown channel',
                      (errors[0].value.data, errors[0].value.value))
        self.assertEqual(1, len(errors))

        # Now deal with the results on the endpoint side.
        f = self.failureResultOf(connected)
        f.trap(ConchError)
        self.assertEqual('unknown channel', f.value.value)

        # Nothing useful can be done with the connection at this point, so the
        # endpoint should close it.
        self.assertTrue(client.transport.disconnecting)

    def test_publicKeyAuthentication(self):
        """
        If L{SSHCommandClientEndpoint} is initialized with any private keys, it
        will try to use them to authenticate with the SSH server.
        """
        key = Key.fromString(privateDSA_openssh)
        self.setupKeyChecker(self.portal, {self.user: privateDSA_openssh})

        self.realm.channelLookup[b'session'] = WorkingExecSession
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            self.user,
            self.hostname,
            self.port,
            keys=[key],
            knownHosts=self.knownHosts,
            ui=FixedResponseUI(False))

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        protocol = self.successResultOf(connected)
        self.assertNotIdentical(None, protocol.transport)

    def test_agentAuthentication(self):
        """
        If L{SSHCommandClientEndpoint} is initialized with an
        L{SSHAgentClient}, the agent is used to authenticate with the SSH
        server.
        """
        key = Key.fromString(privateRSA_openssh)
        agentServer = SSHAgentServer()
        agentServer.factory = Factory()
        agentServer.factory.keys = {key.blob(): (key, "")}

        self.setupKeyChecker(self.portal, {self.user: privateRSA_openssh})

        agentEndpoint = SingleUseMemoryEndpoint(agentServer)
        endpoint = SSHCommandClientEndpoint.newConnection(
            self.reactor,
            b"/bin/ls -l",
            self.user,
            self.hostname,
            self.port,
            knownHosts=self.knownHosts,
            ui=FixedResponseUI(False),
            agentEndpoint=agentEndpoint)

        self.realm.channelLookup[b'session'] = WorkingExecSession

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.connectedServerAndClient(
            self.factory, self.reactor.tcpClients[0][2])

        # Let the agent client talk with the agent server and the ssh client
        # talk with the ssh server.
        for i in range(14):
            agentEndpoint.pump.pump()
            pump.pump()

        protocol = self.successResultOf(connected)
        self.assertNotIdentical(None, protocol.transport)

    def test_loseConnection(self):
        """
        The transport connected to the protocol has a C{loseConnection} method
        which causes the channel in which the command is running to close and
        the overall connection to be closed.
        """
        self.realm.channelLookup[b'session'] = WorkingExecSession
        endpoint = self.create()

        factory = Factory()
        factory.protocol = Protocol
        connected = endpoint.connect(factory)

        server, client, pump = self.finishConnection()

        protocol = self.successResultOf(connected)
        closed = self.record(server, protocol, 'closed', noArgs=True)
        protocol.transport.loseConnection()
        pump.pump()
        self.assertEqual([None], closed)

        # Let the last bit of network traffic flow.  This lets the server's
        # close acknowledgement through, at which point the client can close
        # the overall SSH connection.
        pump.pump()

        # Nothing useful can be done with the connection at this point, so the
        # endpoint should close it.
        self.assertTrue(client.transport.disconnecting)