Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SSH key support #15

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
package com.github.stefanbirkner.fakesftpserver.rule;

import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import static com.github.marschall.memoryfilesystem.MemoryFileSystemBuilder.newLinux;
import static java.nio.file.FileVisitResult.CONTINUE;
import static java.nio.file.Files.copy;
import static java.nio.file.Files.delete;
import static java.nio.file.Files.exists;
import static java.nio.file.Files.isDirectory;
import static java.nio.file.Files.readAllBytes;
import static java.nio.file.Files.walkFileTree;
import static java.nio.file.Files.write;
import static java.util.Collections.singletonList;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.UserPrincipalLookupService;
import java.nio.file.spi.FileSystemProvider;
import java.security.PublicKey;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static com.github.marschall.memoryfilesystem.MemoryFileSystemBuilder.newLinux;
import static java.nio.file.FileVisitResult.CONTINUE;
import static java.nio.file.Files.*;
import static java.util.Collections.singletonList;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

/**
* Fake SFTP Server Rule is a JUnit rule that runs an in-memory SFTP server
Expand Down Expand Up @@ -178,6 +193,7 @@ public FileVisitResult postVisitDirectory(
}
};
private final Map<String, String> usernamesAndPasswords = new HashMap<>();
private final Map<String, Path> usernamesAndIdentities = new HashMap<>();
private int port = 0;

private FileSystem fileSystem;
Expand Down Expand Up @@ -245,6 +261,27 @@ public FakeSftpServerRule addUser(
return this;
}

/**
* Register a username with its identity key and password. After registering a username
* it is only possible to connect to the server with one of the registered
* username/password or username/identity pairs.
* <p>If {@code addIdentity} is called multiple times with the same username but
* different keys then the last key is effective.</p>
* <p>This method is compatible with {@code addUser} meaning if you call
* both then the last username/password is effective and the last
* username/identity is effective.</p>
* @param username the username.
* @param identityPath path to identity file (e.g. authorized_keys file).
* @return the rule itself.
*/
public FakeSftpServerRule addIdentity(
String username,
Path identityPath
) {
usernamesAndIdentities.put(username, identityPath);
return this;
}

private void restartServer() {
try {
server.stop();
Expand Down Expand Up @@ -429,6 +466,7 @@ private SshServer startServer(
server.setPort(port);
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
server.setPasswordAuthenticator(this::authenticate);
server.setPublickeyAuthenticator(this::authenticatePublicKey);
server.setSubsystemFactories(singletonList(new SftpSubsystemFactory()));
/* When a channel is closed SshServer calls close() on the file system.
* In order to use the file system for multiple channels/sessions we
Expand All @@ -439,18 +477,39 @@ private SshServer startServer(
this.server = server;
return server;
}

private boolean emptySecurity() {
return usernamesAndPasswords.isEmpty() && usernamesAndIdentities.isEmpty();
}

private boolean authenticate(
String username,
String password,
ServerSession session
) {
return usernamesAndPasswords.isEmpty()
return emptySecurity()
|| Objects.equals(
usernamesAndPasswords.get(username),
password
);
}

private boolean authenticatePublicKey(
String username,
PublicKey publicKey,
ServerSession session
) {
if (emptySecurity()) {
return true;
} else if (!usernamesAndIdentities.containsKey(username)) {
return false;
}
Path path = usernamesAndIdentities.get(username);
// don't load authorized keys in strict mode
// strict mode forces checks on 'authorized_keys' files for security
// but this is a test rule and CI builders might not force permissions
return new DefaultAuthorizedKeysAuthenticator(username, path, false).authenticate(username, publicKey, session);
}

private void ensureDirectoryOfPathExists(
Path path
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,66 @@
package com.github.stefanbirkner.fakesftpserver.rule;


import com.jcraft.jsch.*;
import org.apache.commons.io.IOUtils;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import static com.github.stefanbirkner.fakesftpserver.rule.Executor.executeTestThatThrowsExceptionWithRule;
import static com.github.stefanbirkner.fakesftpserver.rule.Executor.executeTestWithRule;
import static com.github.stefanbirkner.fishbowl.Fishbowl.exceptionThrownBy;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.commons.io.IOUtils.toByteArray;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.catchThrowable;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;

import static com.github.stefanbirkner.fakesftpserver.rule.Executor.executeTestThatThrowsExceptionWithRule;
import static com.github.stefanbirkner.fakesftpserver.rule.Executor.executeTestWithRule;
import static com.github.stefanbirkner.fishbowl.Fishbowl.exceptionThrownBy;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.commons.io.IOUtils.toByteArray;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.catchThrowable;
import org.apache.commons.io.IOUtils;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.slf4j.LoggerFactory;

import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;

/* Wording according to the draft:
* http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13
*/
@RunWith(Enclosed.class)
public class FakeSftpServerRuleTest {

private static final org.slf4j.Logger log = LoggerFactory.getLogger(FakeSftpServerRuleTest.class);

private static final byte[] DUMMY_CONTENT = new byte[]{1, 4, 2, 4, 2, 4};
private static final int DUMMY_PORT = 46354;
private static final InputStream DUMMY_STREAM = new ByteArrayInputStream(DUMMY_CONTENT);
private static final JSch JSCH = new JSch();
private static final int TIMEOUT = 500;
private static Path DUMMY_KEY;
private static Path DUMMY_AUTHORIZED_KEYS;
private static Path EMPTY_AUTHORIZED_KEYS;
private static final String DUMMY_KEY_PASSPHRASE = "unittest";

static {
try {
DUMMY_KEY = Paths.get(FakeSftpServerRuleTest.class.getResource("/keys/dummy_key").toURI());
DUMMY_AUTHORIZED_KEYS = Paths.get(FakeSftpServerRuleTest.class.getResource("/keys/dummy_key.pub").toURI());
EMPTY_AUTHORIZED_KEYS = Paths.get(FakeSftpServerRuleTest.class.getResource("/keys/empty_authorized_keys").toURI());
} catch (URISyntaxException e) {
log.error("Error loading SSH keys", e);
}
}

public static class round_trip {
@Test
Expand Down Expand Up @@ -105,6 +130,24 @@ public void the_server_accepts_connections_with_password() {
sftpServer
);
}

@Test
public void the_server_accepts_connections_with_identity() {
FakeSftpServerRule sftpServer = new FakeSftpServerRule();
executeTestWithRule(
() -> {
Session session = createSessionWithIdentity(
sftpServer,
"dummy user",
DUMMY_KEY.toString(),
DUMMY_KEY_PASSPHRASE
);
session.connect(TIMEOUT);
JSCH.removeAllIdentity();
},
sftpServer
);
}
}

public static class server_with_credentials_immediately_set {
Expand Down Expand Up @@ -221,6 +264,99 @@ public void the_last_password_is_effective_if_addUser_is_called_multiple_times()
}
}

public static class server_with_identity_immediately_set {

Path privateKeyPath;
Path authorizedKeysPath;
@Before
public void setupIdentity() throws URISyntaxException {
privateKeyPath = Paths.get(FakeSftpServerRuleTest.class.getResource("/keys/dummy_key").toURI());
authorizedKeysPath = Paths.get(FakeSftpServerRuleTest.class.getResource("/keys/dummy_key.pub").toURI());
}

@Test
public void the_server_accepts_connections_with_correct_identity() {
FakeSftpServerRule sftpServer = new FakeSftpServerRule()
.addIdentity("dummy user", DUMMY_AUTHORIZED_KEYS);
executeTestWithRule(
() -> {
Session session = createSessionWithIdentity(
sftpServer,
"dummy user",
DUMMY_KEY.toString(),
DUMMY_KEY_PASSPHRASE
);
session.connect(TIMEOUT);
JSCH.removeAllIdentity();
},
sftpServer
);
}


@Test
public void the_server_rejects_connections_with_wrong_passphrase() {
FakeSftpServerRule sftpServer = new FakeSftpServerRule()
.addIdentity("dummy user", DUMMY_AUTHORIZED_KEYS);
executeTestWithRule(
() -> {
Session session = createSessionWithIdentity(
sftpServer,
"dummy user",
DUMMY_KEY.toString(),
"invalid"
);
assertAuthenticationFails(
() -> session.connect(TIMEOUT)
);
JSCH.removeAllIdentity();
},
sftpServer
);
}

@Test
public void the_server_rejects_connections_with_wrong_key() {
FakeSftpServerRule sftpServer = new FakeSftpServerRule()
.addIdentity("dummy user", EMPTY_AUTHORIZED_KEYS);
executeTestWithRule(
() -> {
Session session = createSessionWithIdentity(
sftpServer,
"dummy user",
DUMMY_KEY.toString(),
DUMMY_KEY_PASSPHRASE
);
assertAuthenticationFails(
() -> session.connect(TIMEOUT)
);
JSCH.removeAllIdentity();
},
sftpServer
);
}

@Test
public void the_last_key_is_effective_if_addIdentity_is_called_multiple_times() {
FakeSftpServerRule sftpServer = new FakeSftpServerRule()
.addIdentity("dummy user", EMPTY_AUTHORIZED_KEYS)
.addIdentity("dummy user", DUMMY_AUTHORIZED_KEYS);
executeTestWithRule(
() -> {
Session session = createSessionWithIdentity(
sftpServer,
"dummy user",
DUMMY_KEY.toString(),
DUMMY_KEY_PASSPHRASE
);
session.connect(TIMEOUT);
JSCH.removeAllIdentity();
},
sftpServer
);
}
}

private static Session createSessionWithCredentials(
FakeSftpServerRule sftpServer,
String username,
Expand All @@ -230,13 +366,24 @@ private static Session createSessionWithCredentials(
username, password, sftpServer.getPort()
);
}

private static Session createSessionWithIdentity(
FakeSftpServerRule sftpServer,
String username,
String prvkey,
String passphrase
) throws JSchException {
return FakeSftpServerRuleTest.createSessionWithIdentity(
username, prvkey, passphrase, sftpServer.getPort()
);
}

private static void assertAuthenticationFails(
ThrowingCallable connectToServer
) {
assertThatThrownBy(connectToServer)
.isInstanceOf(JSchException.class)
.hasMessage("Auth fail");
.hasMessageMatching("(Auth|USERAUTH) fail");
}
}

Expand Down Expand Up @@ -1133,6 +1280,19 @@ private static Session createSessionWithCredentials(
session.setPassword(password);
return session;
}

private static Session createSessionWithIdentity(
String username,
String prvkey,
String passphrase,
int port
) throws JSchException {
// if you need detailed information add a logger to JSCH
JSCH.addIdentity(prvkey, passphrase);
Session session = JSCH.getSession(username, "127.0.0.1", port);
session.setConfig("StrictHostKeyChecking", "no");
return session;
}

private static byte[] downloadFile(
FakeSftpServerRule server,
Expand Down
Loading