diff options
author | Thomas Wolf <thomas.wolf@paranor.ch> | 2019-06-20 19:43:49 +0200 |
---|---|---|
committer | Thomas Wolf <thomas.wolf@paranor.ch> | 2019-09-02 21:30:25 +0200 |
commit | 124fbbc33a05c177767c5f4233717765acb1ab4d (patch) | |
tree | 2386a51734cfb8b5918b93a4ead05fb34ebe31e1 /org.eclipse.jgit.ssh.apache.test | |
parent | 8c74a543155ceff5828bbc5c3c72366729ea23c5 (diff) | |
download | jgit-124fbbc33a05c177767c5f4233717765acb1ab4d.tar.gz jgit-124fbbc33a05c177767c5f4233717765acb1ab4d.zip |
sshd: configurable server key verification
Provide a wrapper interface and change the implementation such that
a client can substitute its own database of known hosts keys instead
of the default file-based mechanism.
Bug: 547619
Change-Id: Ifc25a4519fa5bcf7bb8541b9f3e2de15215e3d66
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
Diffstat (limited to 'org.eclipse.jgit.ssh.apache.test')
-rw-r--r-- | org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF | 9 | ||||
-rw-r--r-- | org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java | 221 |
2 files changed, 229 insertions, 1 deletions
diff --git a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF index 504a20eae4..af0ef2f219 100644 --- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF @@ -7,7 +7,14 @@ Bundle-Version: 5.5.0.qualifier Bundle-Vendor: %Bundle-Vendor Bundle-Localization: plugin Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Import-Package: org.eclipse.jgit.api.errors;version="[5.5.0,5.6.0)", +Import-Package: org.apache.sshd.common;version="[2.2.0,2.3.0)", + org.apache.sshd.common.auth;version="[2.2.0,2.3.0)", + org.apache.sshd.common.config.keys;version="[2.2.0,2.3.0)", + org.apache.sshd.common.keyprovider;version="[2.2.0,2.3.0)", + org.apache.sshd.common.session;version="[2.2.0,2.3.0)", + org.apache.sshd.common.util.net;version="[2.2.0,2.3.0)", + org.apache.sshd.common.util.security;version="[2.2.0,2.3.0)", + org.eclipse.jgit.api.errors;version="[5.5.0,5.6.0)", org.eclipse.jgit.internal.transport.sshd.proxy;version="[5.5.0,5.6.0)", org.eclipse.jgit.junit;version="[5.5.0,5.6.0)", org.eclipse.jgit.junit.ssh;version="[5.5.0,5.6.0)", diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java new file mode 100644 index 0000000000..c0dc2cf20e --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/NoFilesSshTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2019 Thomas Wolf <thomas.wolf@paranor.ch> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.transport.sshd; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.keyprovider.KeyIdentityProvider; +import org.apache.sshd.common.session.SessionContext; +import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.ssh.SshTestHarness; +import org.eclipse.jgit.util.FS; +import org.junit.After; +import org.junit.Test; + +/** + * Test for using the SshdSessionFactory without files in ~/.ssh but with an + * in-memory setup. + */ +public class NoFilesSshTest extends SshTestHarness { + + + private PublicKey testServerKey; + + private KeyPair testUserKey; + + @Override + protected SshSessionFactory createSessionFactory() { + SshdSessionFactory result = new SshdSessionFactory(new JGitKeyCache(), + null) { + + @Override + protected File getSshConfig(File dir) { + return null; + } + + @Override + protected ServerKeyDatabase getServerKeyDatabase(File homeDir, + File dir) { + return new ServerKeyDatabase() { + + @Override + public List<PublicKey> lookup(String connectAddress, + InetSocketAddress remoteAddress, + Configuration config) { + return Collections.singletonList(testServerKey); + } + + @Override + public boolean accept(String connectAddress, + InetSocketAddress remoteAddress, + PublicKey serverKey, Configuration config, + CredentialsProvider provider) { + return KeyUtils.compareKeys(serverKey, testServerKey); + } + + }; + } + + @Override + protected Iterable<KeyPair> getDefaultKeys(File dir) { + // This would work for this simple test case: + // return Collections.singletonList(testUserKey); + // But let's see if we can check the host and username that's used. + // For that, we need access to the sshd SessionContext: + return new KeyAuthenticator(); + } + + @Override + protected String getDefaultPreferredAuthentications() { + return "publickey"; + } + }; + + // The home directory is mocked at this point! + result.setHomeDirectory(FS.DETECTED.userHome()); + result.setSshDirectory(sshDir); + return result; + } + + private class KeyAuthenticator implements KeyIdentityProvider, Iterable<KeyPair> { + + @Override + public Iterator<KeyPair> iterator() { + // Should not be called. The use of the Iterable interface in + // SshdSessionFactory.getDefaultKeys() made sense in sshd 2.0.0, + // but sshd 2.2.0 added the SessionContext, which although good + // (without it we couldn't check here) breaks the Iterable analogy. + // But we're stuck now with that interface for getDefaultKeys, and + // so this override throwing an exception is unfortunately needed. + throw new UnsupportedOperationException(); + } + + @Override + public Iterable<KeyPair> loadKeys(SessionContext session) + throws IOException, GeneralSecurityException { + if (!TEST_USER.equals(session.getUsername())) { + return Collections.emptyList(); + } + SshdSocketAddress remoteAddress = SshdSocketAddress + .toSshdSocketAddress(session.getRemoteAddress()); + switch (remoteAddress.getHostName()) { + case "localhost": + case "127.0.0.1": + return Collections.singletonList(testUserKey); + default: + return Collections.emptyList(); + } + } + } + + @After + public void cleanUp() { + testServerKey = null; + testUserKey = null; + } + + @Override + protected void installConfig(String... config) { + File configFile = new File(sshDir, Constants.CONFIG); + if (config != null) { + try { + Files.write(configFile.toPath(), Arrays.asList(config)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + private KeyPair load(Path path) throws Exception { + try (InputStream in = Files.newInputStream(path)) { + return SecurityUtils + .loadKeyPairIdentities(null, + NamedResource.ofName(path.toString()), in, null) + .iterator().next(); + } + } + + @Test + public void testCloneWithBuiltInKeys() throws Exception { + // This test should fail unless our in-memory setup is taken: no + // known_hosts file, and a config that specifies a non-existing key. + File newHostKey = new File(getTemporaryDirectory(), "newhostkey"); + copyTestResource("id_ed25519", newHostKey); + server.addHostKey(newHostKey.toPath(), true); + testServerKey = load(newHostKey.toPath()).getPublic(); + assertTrue(newHostKey.delete()); + testUserKey = load(privateKey1.getAbsoluteFile().toPath()); + assertNotNull(testServerKey); + assertNotNull(testUserKey); + cloneWith( + "ssh://" + TEST_USER + "@localhost:" + testPort + + "/doesntmatter", + new File(getTemporaryDirectory(), "cloned"), null, // + "Host localhost", // + "IdentityFile " + + new File(sshDir, "does_not_exist").getAbsolutePath()); + } + +} |