Skip to content

Commit 49c4aad

Browse files
committed
Add public key support
1 parent 2f6b2ea commit 49c4aad

File tree

5 files changed

+299
-28
lines changed

5 files changed

+299
-28
lines changed

src/main/java/com/github/stefanbirkner/fakesftpserver/rule/FakeSftpServerRule.java

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,44 @@
11
package com.github.stefanbirkner.fakesftpserver.rule;
22

3-
import org.apache.sshd.server.SshServer;
4-
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
5-
import org.apache.sshd.server.session.ServerSession;
6-
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
7-
import org.junit.rules.TestRule;
8-
import org.junit.runner.Description;
9-
import org.junit.runners.model.Statement;
3+
import static com.github.marschall.memoryfilesystem.MemoryFileSystemBuilder.newLinux;
4+
import static java.nio.file.FileVisitResult.CONTINUE;
5+
import static java.nio.file.Files.copy;
6+
import static java.nio.file.Files.delete;
7+
import static java.nio.file.Files.exists;
8+
import static java.nio.file.Files.isDirectory;
9+
import static java.nio.file.Files.readAllBytes;
10+
import static java.nio.file.Files.walkFileTree;
11+
import static java.nio.file.Files.write;
12+
import static java.util.Collections.singletonList;
1013

1114
import java.io.IOException;
1215
import java.io.InputStream;
1316
import java.nio.charset.Charset;
14-
import java.nio.file.*;
17+
import java.nio.file.FileStore;
18+
import java.nio.file.FileSystem;
19+
import java.nio.file.FileVisitResult;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.nio.file.PathMatcher;
23+
import java.nio.file.SimpleFileVisitor;
24+
import java.nio.file.WatchService;
1525
import java.nio.file.attribute.BasicFileAttributes;
1626
import java.nio.file.attribute.UserPrincipalLookupService;
1727
import java.nio.file.spi.FileSystemProvider;
28+
import java.security.PublicKey;
1829
import java.util.HashMap;
1930
import java.util.Map;
2031
import java.util.Objects;
2132
import java.util.Set;
2233

23-
import static com.github.marschall.memoryfilesystem.MemoryFileSystemBuilder.newLinux;
24-
import static java.nio.file.FileVisitResult.CONTINUE;
25-
import static java.nio.file.Files.*;
26-
import static java.util.Collections.singletonList;
34+
import org.apache.sshd.server.SshServer;
35+
import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator;
36+
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
37+
import org.apache.sshd.server.session.ServerSession;
38+
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
39+
import org.junit.rules.TestRule;
40+
import org.junit.runner.Description;
41+
import org.junit.runners.model.Statement;
2742

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

183199
private FileSystem fileSystem;
@@ -245,6 +261,27 @@ public FakeSftpServerRule addUser(
245261
return this;
246262
}
247263

264+
/**
265+
* Register a username with its identity key and password. After registering a username
266+
* it is only possible to connect to the server with one of the registered
267+
* username/password or username/identity pairs.
268+
* <p>If {@code addIdentity} is called multiple times with the same username but
269+
* different keys then the last key is effective.</p>
270+
* <p>This method is compatible with {@code addUser} meaning if you call
271+
* both then the last username/password is effective and the last
272+
* username/identity is effective.</p>
273+
* @param username the username.
274+
* @param identityPath path to identity file (e.g. authorized_keys file).
275+
* @return the rule itself.
276+
*/
277+
public FakeSftpServerRule addIdentity(
278+
String username,
279+
Path identityPath
280+
) {
281+
usernamesAndIdentities.put(username, identityPath);
282+
return this;
283+
}
284+
248285
private void restartServer() {
249286
try {
250287
server.stop();
@@ -429,6 +466,7 @@ private SshServer startServer(
429466
server.setPort(port);
430467
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
431468
server.setPasswordAuthenticator(this::authenticate);
469+
server.setPublickeyAuthenticator(this::authenticatePublicKey);
432470
server.setSubsystemFactories(singletonList(new SftpSubsystemFactory()));
433471
/* When a channel is closed SshServer calls close() on the file system.
434472
* In order to use the file system for multiple channels/sessions we
@@ -439,18 +477,36 @@ private SshServer startServer(
439477
this.server = server;
440478
return server;
441479
}
480+
481+
private boolean emptySecurity() {
482+
return usernamesAndPasswords.isEmpty() && usernamesAndIdentities.isEmpty();
483+
}
442484

443485
private boolean authenticate(
444486
String username,
445487
String password,
446488
ServerSession session
447489
) {
448-
return usernamesAndPasswords.isEmpty()
490+
return emptySecurity()
449491
|| Objects.equals(
450492
usernamesAndPasswords.get(username),
451493
password
452494
);
453495
}
496+
497+
private boolean authenticatePublicKey(
498+
String username,
499+
PublicKey publicKey,
500+
ServerSession session
501+
) {
502+
if (emptySecurity()) {
503+
return true;
504+
} else if (!usernamesAndIdentities.containsKey(username)) {
505+
return false;
506+
}
507+
Path path = usernamesAndIdentities.get(username);
508+
return new DefaultAuthorizedKeysAuthenticator(username, path, true).authenticate(username, publicKey, session);
509+
}
454510

455511
private void ensureDirectoryOfPathExists(
456512
Path path

src/test/java/com/github/stefanbirkner/fakesftpserver/rule/FakeSftpServerRuleTest.java

Lines changed: 175 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,66 @@
11
package com.github.stefanbirkner.fakesftpserver.rule;
22

33

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

1113
import java.io.ByteArrayInputStream;
1214
import java.io.IOException;
1315
import java.io.InputStream;
1416
import java.net.ConnectException;
17+
import java.net.URISyntaxException;
1518
import java.nio.file.Path;
1619
import java.nio.file.Paths;
1720
import java.util.Vector;
1821
import java.util.concurrent.atomic.AtomicInteger;
1922

20-
import static com.github.stefanbirkner.fakesftpserver.rule.Executor.executeTestThatThrowsExceptionWithRule;
21-
import static com.github.stefanbirkner.fakesftpserver.rule.Executor.executeTestWithRule;
22-
import static com.github.stefanbirkner.fishbowl.Fishbowl.exceptionThrownBy;
23-
import static java.nio.charset.StandardCharsets.UTF_8;
24-
import static org.apache.commons.io.IOUtils.toByteArray;
25-
import static org.assertj.core.api.Assertions.assertThat;
26-
import static org.assertj.core.api.Assertions.assertThatThrownBy;
27-
import static org.assertj.core.api.Assertions.catchThrowable;
23+
import org.apache.commons.io.IOUtils;
24+
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
25+
import org.junit.Before;
26+
import org.junit.Test;
27+
import org.junit.experimental.runners.Enclosed;
28+
import org.junit.runner.RunWith;
29+
import org.slf4j.LoggerFactory;
30+
31+
import com.jcraft.jsch.ChannelSftp;
32+
import com.jcraft.jsch.JSch;
33+
import com.jcraft.jsch.JSchException;
34+
import com.jcraft.jsch.Session;
35+
import com.jcraft.jsch.SftpException;
2836

2937
/* Wording according to the draft:
3038
* http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13
3139
*/
3240
@RunWith(Enclosed.class)
3341
public class FakeSftpServerRuleTest {
42+
43+
private static final org.slf4j.Logger log = LoggerFactory.getLogger(FakeSftpServerRuleTest.class);
44+
3445
private static final byte[] DUMMY_CONTENT = new byte[]{1, 4, 2, 4, 2, 4};
3546
private static final int DUMMY_PORT = 46354;
3647
private static final InputStream DUMMY_STREAM = new ByteArrayInputStream(DUMMY_CONTENT);
3748
private static final JSch JSCH = new JSch();
3849
private static final int TIMEOUT = 200;
50+
private static Path DUMMY_KEY;
51+
private static Path DUMMY_AUTHORIZED_KEYS;
52+
private static Path EMPTY_AUTHORIZED_KEYS;
53+
private static final String DUMMY_KEY_PASSPHRASE = "unittest";
54+
55+
static {
56+
try {
57+
DUMMY_KEY = Paths.get(FakeSftpServerRuleTest.class.getResource("/keys/dummy_key").toURI());
58+
DUMMY_AUTHORIZED_KEYS = Paths.get(FakeSftpServerRuleTest.class.getResource("/keys/dummy_key.pub").toURI());
59+
EMPTY_AUTHORIZED_KEYS = Paths.get(FakeSftpServerRuleTest.class.getResource("/keys/empty_authorized_keys").toURI());
60+
} catch (URISyntaxException e) {
61+
log.error("Error loading SSH keys", e);
62+
}
63+
}
3964

4065
public static class round_trip {
4166
@Test
@@ -105,6 +130,24 @@ public void the_server_accepts_connections_with_password() {
105130
sftpServer
106131
);
107132
}
133+
134+
@Test
135+
public void the_server_accepts_connections_with_identity() {
136+
FakeSftpServerRule sftpServer = new FakeSftpServerRule();
137+
executeTestWithRule(
138+
() -> {
139+
Session session = createSessionWithIdentity(
140+
sftpServer,
141+
"dummy user",
142+
DUMMY_KEY.toString(),
143+
DUMMY_KEY_PASSPHRASE
144+
);
145+
session.connect(TIMEOUT);
146+
JSCH.removeAllIdentity();
147+
},
148+
sftpServer
149+
);
150+
}
108151
}
109152

110153
public static class server_with_credentials_immediately_set {
@@ -221,6 +264,99 @@ public void the_last_password_is_effective_if_addUser_is_called_multiple_times()
221264
}
222265
}
223266

267+
public static class server_with_identity_immediately_set {
268+
269+
Path privateKeyPath;
270+
Path authorizedKeysPath;
271+
@Before
272+
public void setupIdentity() throws URISyntaxException {
273+
privateKeyPath = Paths.get(FakeSftpServerRuleTest.class.getResource("/keys/dummy_key").toURI());
274+
authorizedKeysPath = Paths.get(FakeSftpServerRuleTest.class.getResource("/keys/dummy_key.pub").toURI());
275+
}
276+
277+
@Test
278+
public void the_server_accepts_connections_with_correct_identity() {
279+
FakeSftpServerRule sftpServer = new FakeSftpServerRule()
280+
.addIdentity("dummy user", DUMMY_AUTHORIZED_KEYS);
281+
executeTestWithRule(
282+
() -> {
283+
Session session = createSessionWithIdentity(
284+
sftpServer,
285+
"dummy user",
286+
DUMMY_KEY.toString(),
287+
DUMMY_KEY_PASSPHRASE
288+
);
289+
session.connect(TIMEOUT);
290+
JSCH.removeAllIdentity();
291+
},
292+
sftpServer
293+
);
294+
}
295+
296+
297+
@Test
298+
public void the_server_rejects_connections_with_wrong_passphrase() {
299+
FakeSftpServerRule sftpServer = new FakeSftpServerRule()
300+
.addIdentity("dummy user", DUMMY_AUTHORIZED_KEYS);
301+
executeTestWithRule(
302+
() -> {
303+
Session session = createSessionWithIdentity(
304+
sftpServer,
305+
"dummy user",
306+
DUMMY_KEY.toString(),
307+
"invalid"
308+
);
309+
assertAuthenticationFails(
310+
() -> session.connect(TIMEOUT)
311+
);
312+
JSCH.removeAllIdentity();
313+
},
314+
sftpServer
315+
);
316+
}
317+
318+
@Test
319+
public void the_server_rejects_connections_with_wrong_key() {
320+
FakeSftpServerRule sftpServer = new FakeSftpServerRule()
321+
.addIdentity("dummy user", EMPTY_AUTHORIZED_KEYS);
322+
executeTestWithRule(
323+
() -> {
324+
Session session = createSessionWithIdentity(
325+
sftpServer,
326+
"dummy user",
327+
DUMMY_KEY.toString(),
328+
DUMMY_KEY_PASSPHRASE
329+
);
330+
assertAuthenticationFails(
331+
() -> session.connect(TIMEOUT)
332+
);
333+
JSCH.removeAllIdentity();
334+
},
335+
sftpServer
336+
);
337+
}
338+
339+
@Test
340+
public void the_last_key_is_effective_if_addIdentity_is_called_multiple_times() {
341+
FakeSftpServerRule sftpServer = new FakeSftpServerRule()
342+
.addIdentity("dummy user", EMPTY_AUTHORIZED_KEYS)
343+
.addIdentity("dummy user", DUMMY_AUTHORIZED_KEYS);
344+
executeTestWithRule(
345+
() -> {
346+
Session session = createSessionWithIdentity(
347+
sftpServer,
348+
"dummy user",
349+
DUMMY_KEY.toString(),
350+
DUMMY_KEY_PASSPHRASE
351+
);
352+
session.connect(TIMEOUT);
353+
JSCH.removeAllIdentity();
354+
},
355+
sftpServer
356+
);
357+
}
358+
}
359+
224360
private static Session createSessionWithCredentials(
225361
FakeSftpServerRule sftpServer,
226362
String username,
@@ -230,13 +366,24 @@ private static Session createSessionWithCredentials(
230366
username, password, sftpServer.getPort()
231367
);
232368
}
369+
370+
private static Session createSessionWithIdentity(
371+
FakeSftpServerRule sftpServer,
372+
String username,
373+
String prvkey,
374+
String passphrase
375+
) throws JSchException {
376+
return FakeSftpServerRuleTest.createSessionWithIdentity(
377+
username, prvkey, passphrase, sftpServer.getPort()
378+
);
379+
}
233380

234381
private static void assertAuthenticationFails(
235382
ThrowingCallable connectToServer
236383
) {
237384
assertThatThrownBy(connectToServer)
238385
.isInstanceOf(JSchException.class)
239-
.hasMessage("Auth fail");
386+
.hasMessageMatching("(Auth|USERAUTH) fail");
240387
}
241388
}
242389

@@ -1133,6 +1280,19 @@ private static Session createSessionWithCredentials(
11331280
session.setPassword(password);
11341281
return session;
11351282
}
1283+
1284+
private static Session createSessionWithIdentity(
1285+
String username,
1286+
String prvkey,
1287+
String passphrase,
1288+
int port
1289+
) throws JSchException {
1290+
// if you need detailed information add a logger to JSCH
1291+
JSCH.addIdentity(prvkey, passphrase);
1292+
Session session = JSCH.getSession(username, "127.0.0.1", port);
1293+
session.setConfig("StrictHostKeyChecking", "no");
1294+
return session;
1295+
}
11361296

11371297
private static byte[] downloadFile(
11381298
FakeSftpServerRule server,

0 commit comments

Comments
 (0)