You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2015/06/22 11:28:23 UTC
[2/2] mina-sshd git commit: [SSHD-498] Add support for loading
client/server identity files
[SSHD-498] Add support for loading client/server identity files
Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/612a2f47
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/612a2f47
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/612a2f47
Branch: refs/heads/master
Commit: 612a2f47a310abefd00ab08ffd54ef55f990afda
Parents: 66698ca
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Mon Jun 22 12:28:07 2015 +0300
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Mon Jun 22 12:28:07 2015 +0300
----------------------------------------------------------------------
.../java/org/apache/sshd/client/SshClient.java | 189 +++++-----
.../java/org/apache/sshd/client/SshKeyScan.java | 18 +-
.../sshd/client/config/keys/ClientIdentity.java | 343 +++++++++++++++++++
.../sshd/common/config/SshConfigFileReader.java | 11 +-
.../common/config/keys/BuiltinIdentities.java | 197 +++++++++++
.../sshd/common/config/keys/Identity.java | 40 +++
.../sshd/common/config/keys/IdentityUtils.java | 143 ++++++++
.../sshd/common/config/keys/KeyUtils.java | 98 +++++-
.../org/apache/sshd/common/scp/ScpHelper.java | 8 +-
.../apache/sshd/common/util/SecurityUtils.java | 23 ++
.../org/apache/sshd/common/util/io/IoUtils.java | 25 +-
.../DefaultAuthorizedKeysAuthenticator.java | 38 +-
.../sshd/server/config/keys/ServerIdentity.java | 200 +++++++++++
.../sshd/server/session/ServerSession.java | 2 +-
.../client/config/keys/ClientIdentityTest.java | 101 ++++++
.../config/keys/BuiltinIdentitiesTest.java | 106 ++++++
.../server/config/keys/ServerIdentityTest.java | 103 ++++++
.../org/apache/sshd/client/config/keys/id_dsa | 12 +
.../org/apache/sshd/client/config/keys/id_ecdsa | 5 +
.../apache/sshd/client/config/keys/id_rsa_key | 27 ++
.../sshd/server/config/keys/ssh_host_dsa_key | 12 +
.../sshd/server/config/keys/ssh_host_ecdsa_key | 5 +
.../sshd/server/config/keys/ssh_host_rsa_key | 27 ++
23 files changed, 1580 insertions(+), 153 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 8bea7ed..f50eede 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -19,7 +19,6 @@
package org.apache.sshd.client;
import java.io.BufferedReader;
-import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
@@ -44,6 +43,7 @@ import org.apache.sshd.client.auth.UserAuthPassword;
import org.apache.sshd.client.auth.UserAuthPublicKey;
import org.apache.sshd.client.channel.ChannelShell;
import org.apache.sshd.client.channel.ClientChannel;
+import org.apache.sshd.client.config.keys.ClientIdentity;
import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.client.future.DefaultConnectFuture;
import org.apache.sshd.client.session.ClientConnectionService;
@@ -61,8 +61,6 @@ import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.future.SshFutureListener;
import org.apache.sshd.common.io.IoConnectFuture;
import org.apache.sshd.common.io.IoConnector;
-import org.apache.sshd.common.keyprovider.AbstractFileKeyPairProvider;
-import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.session.AbstractSession;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.SecurityUtils;
@@ -435,112 +433,99 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
root.setLevel(Level.FINEST);
}
- KeyPairProvider provider = null;
- final List<File> files = new ArrayList<File>();
- File f = new File(System.getProperty("user.home"), ".ssh/id_dsa");
- if (f.exists() && f.isFile() && f.canRead()) {
- files.add(f);
- }
- f = new File(System.getProperty("user.home"), ".ssh/id_rsa");
- if (f.exists() && f.isFile() && f.canRead()) {
- files.add(f);
- }
- f = new File(System.getProperty("user.home"), ".ssh/id_ecdsa");
- if (f.exists() && f.isFile() && f.canRead()) {
- files.add(f);
- }
- if (files.size() > 0) {
- // SSHD-292: we need to use a different class to load the FileKeyPairProvider
- // in order to break the link between SshClient and BouncyCastle
- try {
- if (SecurityUtils.isBouncyCastleRegistered()) {
- AbstractFileKeyPairProvider filesProvider=SecurityUtils.createFileKeyPairProvider();
- filesProvider.setFiles(files);
- filesProvider.setPasswordFinder(new FilePasswordProvider() {
- private final BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
- @Override
- public String getPassword(String file) throws IOException {
- System.out.print("Enter password for private key file=" + file + ": ");
- return r.readLine();
- }
- });
- provider = filesProvider;
+ try(SshClient client = SshClient.setUpDefaultClient();
+ BufferedReader stdin = new BufferedReader(new InputStreamReader(new NoCloseInputStream(System.in)))) {
+ if (SecurityUtils.isBouncyCastleRegistered()) {
+ try {
+ ClientIdentity.setKeyPairProvider(client,
+ false, // not strict - even though we should...
+ true, // supportedOnly
+ new FilePasswordProvider() {
+ @Override
+ public String getPassword(String file) throws IOException {
+ System.out.print("Enter password for private key file=" + file + ": ");
+ return stdin.readLine();
+ }
+ });
+ } catch (Throwable t) {
+ System.out.println("Error loading user keys: " + t.getMessage());
}
- } catch (Throwable t) {
- System.out.println("Error loading user keys: " + t.getMessage());
}
- }
-
- SshClient client = SshClient.setUpDefaultClient();
- Map<String,Object> props = client.getProperties();
- props.putAll(options);
-
- client.start();
- client.setKeyPairProvider(provider);
- client.setUserInteraction(new UserInteraction() {
- @Override
- public void welcome(String banner) {
- System.out.println(banner);
- }
-
- @Override
- public String[] interactive(String destination, String name, String instruction, String[] prompt, boolean[] echo) {
- String[] answers = new String[prompt.length];
- try {
- for (int i = 0; i < prompt.length; i++) {
- BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
- System.out.print(prompt[i] + " ");
- answers[i] = r.readLine();
+
+ Map<String,Object> props = client.getProperties();
+ props.putAll(options);
+
+ client.start();
+ client.setUserInteraction(new UserInteraction() {
+ @Override
+ public void welcome(String banner) {
+ System.out.println(banner);
+ }
+
+ @Override
+ public String[] interactive(String destination, String name, String instruction, String[] prompt, boolean[] echo) {
+ String[] answers = new String[prompt.length];
+ try {
+ for (int i = 0; i < prompt.length; i++) {
+ System.out.print(prompt[i] + " ");
+ answers[i] = stdin.readLine();
+ }
+ } catch (IOException e) {
+ // ignored
+ }
+ return answers;
}
- } catch (IOException e) {
- // ignored
+ });
+
+ /*
+ String authSock = System.getenv(SshAgent.SSH_AUTHSOCKET_ENV_NAME);
+ if (authSock == null && provider != null) {
+ Iterable<KeyPair> keys = provider.loadKeys();
+ AgentServer server = new AgentServer();
+ authSock = server.start();
+ SshAgent agent = new AgentClient(authSock);
+ for (KeyPair key : keys) {
+ agent.addIdentity(key, "");
}
- return answers;
- }
- });
-
- /*
- String authSock = System.getenv(SshAgent.SSH_AUTHSOCKET_ENV_NAME);
- if (authSock == null && provider != null) {
- Iterable<KeyPair> keys = provider.loadKeys();
- AgentServer server = new AgentServer();
- authSock = server.start();
- SshAgent agent = new AgentClient(authSock);
- for (KeyPair key : keys) {
- agent.addIdentity(key, "");
+ agent.close();
+ props.put(SshAgent.SSH_AUTHSOCKET_ENV_NAME, authSock);
}
- agent.close();
- props.put(SshAgent.SSH_AUTHSOCKET_ENV_NAME, authSock);
- }
- */
-
- ClientSession session = client.connect(login, host, port).await().getSession();
- session.auth().verify();
-
- if (socksPort >= 0) {
- session.startDynamicPortForwarding(new SshdSocketAddress("localhost", socksPort));
- Thread.sleep(Long.MAX_VALUE);
- } else {
- ClientChannel channel;
- if (command == null) {
- channel = session.createChannel(ClientChannel.CHANNEL_SHELL);
- ((ChannelShell) channel).setAgentForwarding(agentForward);
- channel.setIn(new NoCloseInputStream(System.in));
- } else {
- StringWriter w = new StringWriter();
- for (String cmd : command) {
- w.append(cmd).append(" ");
+ */
+
+ try(ClientSession session = client.connect(login, host, port).await().getSession()) {
+ session.auth().verify();
+
+ if (socksPort >= 0) {
+ session.startDynamicPortForwarding(new SshdSocketAddress("localhost", socksPort));
+ Thread.sleep(Long.MAX_VALUE);
+ } else {
+ ClientChannel channel;
+ if (command == null) {
+ channel = session.createChannel(ClientChannel.CHANNEL_SHELL);
+ ((ChannelShell) channel).setAgentForwarding(agentForward);
+ channel.setIn(new NoCloseInputStream(System.in));
+ } else {
+ StringWriter w = new StringWriter();
+ for (String cmd : command) {
+ w.append(cmd).append(" ");
+ }
+ w.close();
+ channel = session.createChannel(ClientChannel.CHANNEL_EXEC, w.toString());
+ }
+
+ try {
+ channel.setOut(new NoCloseOutputStream(System.out));
+ channel.setErr(new NoCloseOutputStream(System.err));
+ channel.open().await();
+ channel.waitFor(ClientChannel.CLOSED, 0);
+ } finally {
+ channel.close();
+ }
+ session.close(false);
}
- w.close();
- channel = session.createChannel(ClientChannel.CHANNEL_EXEC, w.toString());
+ } finally {
+ client.stop();
}
- channel.setOut(new NoCloseOutputStream(System.out));
- channel.setErr(new NoCloseOutputStream(System.err));
- channel.open().await();
- channel.waitFor(ClientChannel.CLOSED, 0);
- session.close(false);
- client.stop();
}
}
-
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java b/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java
index 2d0d278..eaec00e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java
@@ -60,6 +60,7 @@ import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.SshdSocketAddress;
import org.apache.sshd.common.cipher.ECCurves;
import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.config.keys.BuiltinIdentities;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.PublicKeyEntry;
import org.apache.sshd.common.io.IoSession;
@@ -82,12 +83,11 @@ import org.apache.sshd.common.util.logging.LoggingUtils;
*/
public class SshKeyScan extends AbstractSimplifiedLog
implements Channel, Callable<Void>, ServerKeyVerifier, SessionListener {
- public static final String RSA_KEY_TYPE = "rsa", DSS_KEY_TYPE = "dsa", EC_KEY_TYPE = "ecdsa";
/**
* Default key types if not overridden from the command line
*/
- public static final List<String> DEFAULT_KEY_TYPES =
- Collections.unmodifiableList(Arrays.asList(RSA_KEY_TYPE, EC_KEY_TYPE));
+ public static final List<String> DEFAULT_KEY_TYPES =
+ Collections.unmodifiableList(Arrays.asList(BuiltinIdentities.Constants.RSA, BuiltinIdentities.Constants.ECDSA));
public static final long DEFAULT_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
public static final Level DEFAULT_LEVEL = Level.INFO;
@@ -413,11 +413,11 @@ public class SshKeyScan extends AbstractSimplifiedLog
log(Level.FINE, "Resolve signature factories for " + keyType);
}
- if (RSA_KEY_TYPE.equalsIgnoreCase(keyType)) {
+ if (BuiltinIdentities.Constants.RSA.equalsIgnoreCase(keyType)) {
return Collections.singletonList((NamedFactory<Signature>) BuiltinSignatures.rsa);
- } else if (DSS_KEY_TYPE.equalsIgnoreCase(keyType)) {
+ } else if (BuiltinIdentities.Constants.DSA.equalsIgnoreCase(keyType)) {
return Collections.singletonList((NamedFactory<Signature>) BuiltinSignatures.dsa);
- } else if (EC_KEY_TYPE.equalsIgnoreCase(keyType)) {
+ } else if (BuiltinIdentities.Constants.ECDSA.equalsIgnoreCase(keyType)) {
List<NamedFactory<Signature>> factories = new ArrayList<NamedFactory<Signature>>(ECCurves.NAMES.size());
for (String n : ECCurves.NAMES) {
if (isEnabled(Level.FINER)) {
@@ -464,11 +464,11 @@ public class SshKeyScan extends AbstractSimplifiedLog
log(Level.FINE, "Generate key pairs for " + keyType);
}
- if (RSA_KEY_TYPE.equalsIgnoreCase(keyType)) {
+ if (BuiltinIdentities.Constants.RSA.equalsIgnoreCase(keyType)) {
return Collections.singletonList(KeyUtils.generateKeyPair(KeyPairProvider.SSH_RSA, 1024));
- } else if (DSS_KEY_TYPE.equalsIgnoreCase(keyType)) {
+ } else if (BuiltinIdentities.Constants.DSA.equalsIgnoreCase(keyType)) {
return Collections.singletonList(KeyUtils.generateKeyPair(KeyPairProvider.SSH_DSS, 512));
- } else if (EC_KEY_TYPE.equalsIgnoreCase(keyType)) {
+ } else if (BuiltinIdentities.Constants.ECDSA.equalsIgnoreCase(keyType)) {
if (!SecurityUtils.hasEcc()) {
throw new InvalidKeySpecException("ECC not supported");
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java b/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java
new file mode 100644
index 0000000..a92ca9a
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.config.keys;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.config.keys.BuiltinIdentities;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.IdentityUtils;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.SecurityUtils;
+import org.apache.sshd.common.util.Transformer;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+
+/**
+ * Provides keys loading capability from the user's keys folder - e.g., {@code id_rsa}
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see SecurityUtils#isBouncyCastleRegistered()
+ */
+public final class ClientIdentity {
+ public static final String ID_FILE_PREFIX = "id_", ID_FILE_SUFFIX = "";
+
+ private ClientIdentity() {
+ throw new UnsupportedOperationException("No instance");
+ }
+
+ public static final Transformer<String,String> ID_GENERATOR =
+ new Transformer<String,String>() {
+ @Override
+ public String transform(String input) {
+ return getIdentityFileName(input);
+ }
+ };
+
+ /**
+ * @param name The file name - ignored if {@code null}/empty
+ * @return The identity type - {@code null} if cannot determine it - e.g.,
+ * does not start with the {@link #ID_FILE_PREFIX}
+ */
+ public static String getIdentityType(String name) {
+ if (GenericUtils.isEmpty(name)
+ || (name.length() <= ID_FILE_PREFIX.length())
+ || (!name.startsWith(ID_FILE_PREFIX))) {
+ return null;
+ } else {
+ return name.substring(ID_FILE_PREFIX.length());
+ }
+ }
+
+ public static String getIdentityFileName(NamedResource r) {
+ return getIdentityFileName((r == null) ? null : r.getName());
+ }
+
+ /**
+ * @param type The identity type - e.g., {@code rsa} - ignored
+ * if {@code null}/empty
+ * @return The matching file name for the identity - {@code null}
+ * if no name
+ * @see #ID_FILE_PREFIX
+ * @see #ID_FILE_SUFFIX
+ * @see IdentityUtils#getIdentityFileName(String, String, String)
+ */
+ public static String getIdentityFileName(String type) {
+ return IdentityUtils.getIdentityFileName(ID_FILE_PREFIX, type, ID_FILE_SUFFIX);
+ }
+
+ /**
+ * @param client The {@link SshClient} to updated
+ * @param strict If {@code true} then files that do not have the required
+ * access rights are excluded from consideration
+ * @param supportedOnly If {@code true} then ignore identities that are not
+ * supported internally
+ * @param provider A {@link FilePasswordProvider} - may be {@code null}
+ * if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+ * to {@link FilePasswordProvider#getPassword(String)} is the path of the
+ * file whose key is to be loaded
+ * @param options The {@link LinkOption}s to apply when checking
+ * for existence
+ * @return The updated <tt>client</tt> instance - provided a non-{@code null}
+ * {@link KeyPairProvider} was generated
+ * @throws IOException If failed to access the file system
+ * @throws GeneralSecurityException If failed to load the keys
+ * @see #getDefaultUserIdentitiesFolder()
+ * @see #setKeyPairProvider(SshClient, Path, boolean, boolean, FilePasswordProvider, LinkOption...)
+ */
+ public static <C extends SshClient> C setKeyPairProvider(
+ C client, boolean strict, boolean supportedOnly, FilePasswordProvider provider, LinkOption ... options)
+ throws IOException, GeneralSecurityException {
+ return setKeyPairProvider(client, getDefaultUserIdentitiesFolder(), strict, supportedOnly, provider, options);
+ }
+
+ /**
+ * @param client The {@link SshClient} to updated
+ * @param dir The folder to scan for the built-in identities
+ * @param strict If {@code true} then files that do not have the required
+ * access rights are excluded from consideration
+ * @param supportedOnly If {@code true} then ignore identities that are not
+ * supported internally
+ * @param provider A {@link FilePasswordProvider} - may be {@code null}
+ * if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+ * to {@link FilePasswordProvider#getPassword(String)} is the path of the
+ * file whose key is to be loaded
+ * @param options The {@link LinkOption}s to apply when checking
+ * for existence
+ * @return The updated <tt>client</tt> instance - provided a non-{@code null}
+ * {@link KeyPairProvider} was generated
+ * @throws IOException If failed to access the file system
+ * @throws GeneralSecurityException If failed to load the keys
+ * @see #loadDefaultKeyPairProvider(Path, boolean, boolean, FilePasswordProvider, LinkOption...)
+ */
+ public static <C extends SshClient> C setKeyPairProvider(
+ C client, Path dir, boolean strict, boolean supportedOnly, FilePasswordProvider provider, LinkOption ... options)
+ throws IOException, GeneralSecurityException {
+ KeyPairProvider kpp = loadDefaultKeyPairProvider(dir, strict, supportedOnly, provider, options);
+ if (kpp != null) {
+ client.setKeyPairProvider(kpp);
+ }
+
+ return client;
+ }
+
+ /**
+ * @param strict If {@code true} then files that do not have the required
+ * access rights are excluded from consideration
+ * @param supportedOnly If {@code true} then ignore identities that are not
+ * supported internally
+ * @param provider A {@link FilePasswordProvider} - may be {@code null}
+ * if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+ * to {@link FilePasswordProvider#getPassword(String)} is the path of the
+ * file whose key is to be loaded
+ * @param options The {@link LinkOption}s to apply when checking
+ * for existence
+ * @return A {@link KeyPair} for the identities - {@code null} if no identities
+ * available (e.g., after filtering unsupported ones or strict permissions)
+ * @throws IOException If failed to access the file system
+ * @throws GeneralSecurityException If failed to load the keys
+ * @see #loadDefaultIdentities(Path, boolean, FilePasswordProvider, LinkOption...)
+ * @see #getDefaultUserIdentitiesFolder()
+ */
+ public static KeyPairProvider loadDefaultKeyPairProvider(
+ boolean strict, boolean supportedOnly, FilePasswordProvider provider, LinkOption ... options)
+ throws IOException, GeneralSecurityException {
+ return loadDefaultKeyPairProvider(getDefaultUserIdentitiesFolder(), strict, supportedOnly, provider, options);
+ }
+
+ /**
+ * @param dir The folder to scan for the built-in identities
+ * @param strict If {@code true} then files that do not have the required
+ * access rights are excluded from consideration
+ * @param supportedOnly If {@code true} then ignore identities that are not
+ * supported internally
+ * @param provider A {@link FilePasswordProvider} - may be {@code null}
+ * if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+ * to {@link FilePasswordProvider#getPassword(String)} is the path of the
+ * file whose key is to be loaded
+ * @param options The {@link LinkOption}s to apply when checking
+ * for existence
+ * @return A {@link KeyPair} for the identities - {@code null} if no identities
+ * available (e.g., after filtering unsupported ones or strict permissions)
+ * @throws IOException If failed to access the file system
+ * @throws GeneralSecurityException If failed to load the keys
+ * @see #loadDefaultIdentities(Path, boolean, FilePasswordProvider, LinkOption...)
+ * @see IdentityUtils#createKeyPairProvider(Map, boolean)
+ */
+ public static KeyPairProvider loadDefaultKeyPairProvider(
+ Path dir, boolean strict, boolean supportedOnly, FilePasswordProvider provider, LinkOption ... options)
+ throws IOException, GeneralSecurityException {
+ Map<String,KeyPair> ids = loadDefaultIdentities(dir, strict, provider, options);
+ return IdentityUtils.createKeyPairProvider(ids, supportedOnly);
+ }
+
+ /**
+ * @param strict If {@code true} then files that do not have the required
+ * access rights are excluded from consideration
+ * @param provider A {@link FilePasswordProvider} - may be {@code null}
+ * if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+ * to {@link FilePasswordProvider#getPassword(String)} is the path of the
+ * file whose key is to be loaded
+ * @param options The {@link LinkOption}s to apply when checking
+ * for existence
+ * @return A {@link Map} of the found files where key=identity type (case
+ * <U>insensitive</U>), value=the {@link KeyPair} of the identity
+ * @throws IOException If failed to access the file system
+ * @throws GeneralSecurityException If failed to load the keys
+ * @see #getDefaultUserIdentitiesFolder()
+ * @see #loadDefaultIdentities(Path, boolean, FilePasswordProvider, LinkOption...)
+ */
+ public static Map<String,KeyPair> loadDefaultIdentities(boolean strict, FilePasswordProvider provider, LinkOption ... options)
+ throws IOException, GeneralSecurityException {
+ return loadDefaultIdentities(getDefaultUserIdentitiesFolder(), strict, provider, options);
+ }
+
+ /**
+ * @param dir The folder to scan for the built-in identities
+ * @param strict If {@code true} then files that do not have the required
+ * access rights are excluded from consideration
+ * @param provider A {@link FilePasswordProvider} - may be {@code null}
+ * if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+ * to {@link FilePasswordProvider#getPassword(String)} is the path of the
+ * file whose key is to be loaded
+ * @param options The {@link LinkOption}s to apply when checking
+ * for existence
+ * @return A {@link Map} of the found files where key=identity type (case
+ * <U>insensitive</U>), value=the {@link KeyPair} of the identity
+ * @throws IOException If failed to access the file system
+ * @throws GeneralSecurityException If failed to load the keys
+ * @see #loadIdentities(Path, boolean, Collection, Transformer, FilePasswordProvider, LinkOption...)
+ * @see BuiltinIdentities
+ */
+ public static Map<String,KeyPair> loadDefaultIdentities(Path dir, boolean strict, FilePasswordProvider provider, LinkOption ... options)
+ throws IOException, GeneralSecurityException {
+ return loadIdentities(dir, strict, BuiltinIdentities.NAMES, ID_GENERATOR, provider, options);
+ }
+
+ /**
+ * @return The {@link Path} of the default folder where client identity
+ * files are kept
+ * @throws IOException If not value for {@code user.home} system property
+ */
+ public static Path getDefaultUserIdentitiesFolder() throws IOException {
+ String userHome = System.getProperty("user.home");
+ if (GenericUtils.isEmpty(userHome)) {
+ throw new FileNotFoundException("No user home value");
+ }
+
+ Path homeDir = new File(userHome).toPath();
+ return homeDir.resolve(PublicKeyEntry.STD_KEYFILE_FOLDER_NAME);
+ }
+
+ /**
+ * Scans a folder and loads all available identity files
+ * @param dir The {@link Path} of the folder to scan - ignored if not exists
+ * @param strict If {@code true} then files that do not have the required
+ * access rights are excluded from consideration
+ * @param types The identity types - ignored if {@code null}/empty
+ * @param idGenerator A {@link Transformer} to derive the file name
+ * holding the specified type
+ * @param provider A {@link FilePasswordProvider} - may be {@code null}
+ * if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+ * to {@link FilePasswordProvider#getPassword(String)} is the path of the
+ * file whose key is to be loaded
+ * @param options The {@link LinkOption}s to apply when checking
+ * for existence
+ * @return A {@link Map} of the found files where key=identity type (case
+ * <U>insensitive</U>), value=the {@link KeyPair} of the identity
+ * @throws IOException If failed to access the file system
+ * @throws GeneralSecurityException If failed to load the keys
+ * @see #scanIdentitiesFolder(Path, Collection, Transformer, LinkOption...)
+ * @see IdentityUtils#loadIdentities(Map, FilePasswordProvider, OpenOption...)
+ */
+ public static Map<String,KeyPair> loadIdentities(
+ Path dir, boolean strict, Collection<String> types, Transformer<String,String> idGenerator, FilePasswordProvider provider, LinkOption ... options)
+ throws IOException, GeneralSecurityException {
+ Map<String,Path> paths = scanIdentitiesFolder(dir, strict, types, idGenerator, options);
+ return IdentityUtils.loadIdentities(paths, provider, IoUtils.EMPTY_OPEN_OPTIONS);
+ }
+
+ /**
+ * Scans a folder for possible identity files
+ * @param dir The {@link Path} of the folder to scan - ignored if not exists
+ * @param strict If {@code true} then files that do not have the required
+ * access rights are excluded from consideration
+ * @param types The identity types - ignored if {@code null}/empty
+ * @param idGenerator A {@link Transformer} to derive the file name
+ * holding the specified type
+ * @param options The {@link LinkOption}s to apply when checking
+ * for existence
+ * @return A {@link Map} of the found files where key=identity type (case
+ * <U>insensitive</U>), value=the {@link Path} of the file holding the key
+ * @throws IOException If failed to access the file system
+ * @see KeyUtils#validateStrictKeyFilePermissions(Path, LinkOption...)
+ */
+ public static Map<String,Path> scanIdentitiesFolder(
+ Path dir, boolean strict, Collection<String> types, Transformer<String,String> idGenerator, LinkOption ... options)
+ throws IOException {
+ if (GenericUtils.isEmpty(types)) {
+ return Collections.emptyMap();
+ }
+
+ if (!Files.exists(dir, options)) {
+ return Collections.emptyMap();
+ }
+
+ ValidateUtils.checkTrue(Files.isDirectory(dir, options), "Not a directory: %s", dir);
+
+ Map<String,Path> paths = new TreeMap<String, Path>(String.CASE_INSENSITIVE_ORDER);
+ for (String t : types) {
+ String fileName = idGenerator.transform(t);
+ Path p = dir.resolve(fileName);
+ if (!Files.exists(p, options)) {
+ continue;
+ }
+
+ if (strict) {
+ PosixFilePermission perm = KeyUtils.validateStrictKeyFilePermissions(p, options);
+ if (perm != null) {
+ continue;
+ }
+ }
+
+ Path prev = paths.put(t, p);
+ ValidateUtils.checkTrue(prev == null, "Multiple mappings for type=%s", t);
+ }
+
+ return paths;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/common/config/SshConfigFileReader.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/SshConfigFileReader.java b/sshd-core/src/main/java/org/apache/sshd/common/config/SshConfigFileReader.java
index 92587a3..95641fa 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/config/SshConfigFileReader.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/SshConfigFileReader.java
@@ -29,6 +29,9 @@ import java.io.Reader;
import java.io.StreamCorruptedException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -58,6 +61,7 @@ import org.apache.sshd.common.signature.SignatureFactory;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.Transformer;
import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.common.util.io.NoCloseInputStream;
import org.apache.sshd.common.util.io.NoCloseReader;
import org.apache.sshd.server.ServerBuilder;
@@ -87,7 +91,6 @@ public class SshConfigFileReader {
public static final boolean DEFAULT_X11_FORWARDING_VALUE=parseBooleanValue(DEFAULT_X11_FORWARDING);
public static final String MAX_SESSIONS_CONFIG_PROP="MaxSessions";
public static final int DEFAULT_MAX_SESSIONS=10;
- public static final String HOST_KEY_CONFIG_PROP="HostKey";
public static final String PASSWORD_AUTH_CONFIG_PROP="PasswordAuthentication";
public static final String DEFAULT_PASSWORD_AUTH="no";
public static final boolean DEFAULT_PASSWORD_AUTH_VALUE=parseBooleanValue(DEFAULT_PASSWORD_AUTH);
@@ -143,7 +146,11 @@ public class SshConfigFileReader {
public static final String SUBSYSTEM_CONFIG_PROP="Subsystem";
public static final Properties readConfigFile(File file) throws IOException {
- try(InputStream input=new FileInputStream(file)) {
+ return readConfigFile(file.toPath(), IoUtils.EMPTY_OPEN_OPTIONS);
+ }
+
+ public static final Properties readConfigFile(Path path, OpenOption ... options) throws IOException {
+ try(InputStream input = Files.newInputStream(path, options)) {
return readConfigFile(input, true);
}
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java
new file mode 100644
index 0000000..1755f31
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.config.keys;
+
+import java.security.Key;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.DSAPrivateKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.SecurityUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public enum BuiltinIdentities implements Identity {
+ RSA(Constants.RSA, RSAPublicKey.class, RSAPrivateKey.class),
+ DSA(Constants.DSA, DSAPublicKey.class, DSAPrivateKey.class),
+ ECDSA(Constants.ECDSA, "EC", ECPublicKey.class, ECPrivateKey.class) {
+ @Override
+ public boolean isSupported() {
+ return SecurityUtils.hasEcc();
+ }
+ };
+
+ private final String name, algorithm;
+ private final Class<? extends PublicKey> pubType;
+ private final Class<? extends PrivateKey> prvType;
+
+ @Override
+ public final String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return true;
+ }
+
+ @Override
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ @Override
+ public final Class<? extends PublicKey> getPublicKeyType() {
+ return pubType;
+ }
+
+ @Override
+ public final Class<? extends PrivateKey> getPrivateKeyType() {
+ return prvType;
+ }
+
+ BuiltinIdentities(String type, Class<? extends PublicKey> pubType, Class<? extends PrivateKey> prvType) {
+ this(type, type, pubType, prvType);
+ }
+
+ BuiltinIdentities(String name, String algorithm, Class<? extends PublicKey> pubType, Class<? extends PrivateKey> prvType) {
+ this.name = name.toLowerCase();
+ this.algorithm = algorithm.toUpperCase();
+ this.pubType = pubType;
+ this.prvType = prvType;
+ }
+
+ public static final Set<BuiltinIdentities> VALUES =
+ Collections.unmodifiableSet(EnumSet.allOf(BuiltinIdentities.class));
+
+ public static final Set<String> NAMES =
+ Collections.unmodifiableSet(new TreeSet<String>(String.CASE_INSENSITIVE_ORDER) {
+ private static final long serialVersionUID = 1L; // we're not serializing it
+
+ {
+ addAll(NamedResource.Utils.getNameList(VALUES));
+ }
+ });
+
+ /**
+ * @param name The identity name - ignored if {@code null}/empty
+ * @return The matching {@link BuiltinIdentities} whose {@link #getName()}
+ * value matches case <U>insensitive</U> or {@code null} if no match found
+ */
+ public static final BuiltinIdentities fromName(String name) {
+ return NamedResource.Utils.findByName(name, String.CASE_INSENSITIVE_ORDER, VALUES);
+ }
+
+ /**
+ * @param algorithm The algorithm - ignored if {@code null}/empty
+ * @return The matching {@link BuiltinIdentities} whose {@link #getAlgorithm()}
+ * value matches case <U>insensitive</U> or {@code null} if no match found
+ */
+ public static final BuiltinIdentities fromAlgorithm(String algorithm) {
+ if (GenericUtils.isEmpty(algorithm)) {
+ return null;
+ }
+
+ for (BuiltinIdentities id : VALUES) {
+ if (algorithm.equalsIgnoreCase(id.getAlgorithm())) {
+ return id;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param kp The {@link KeyPair} - ignored if {@code null}
+ * @return The matching {@link BuiltinIdentities} provided <U>both</U>
+ * public and public keys are of the same type - {@code null} if no
+ * match could be found
+ * @see #fromKey(Key)
+ */
+ public static final BuiltinIdentities fromKeyPair(KeyPair kp) {
+ if (kp == null) {
+ return null;
+ }
+
+ BuiltinIdentities i1 = fromKey(kp.getPublic());
+ BuiltinIdentities i2 = fromKey(kp.getPrivate());
+ if (Objects.equals(i1, i2)) {
+ return i1;
+ } else {
+ return null; // some kind of mixed keys...
+ }
+ }
+
+ /**
+ * @param key The {@link Key} instance - ignored if {@code null}
+ * @return The matching {@link BuiltinIdentities} whose either public or
+ * private key type matches the requested one or {@code null} if no match found
+ * @see #fromKeyType(Class)
+ */
+ public static final BuiltinIdentities fromKey(Key key) {
+ return fromKeyType((key == null) ? null : key.getClass());
+ }
+
+ /**
+ * @param clazz The key type - ignored if {@code null} or not
+ * a {@link Key} class
+ * @return The matching {@link BuiltinIdentities} whose either public or
+ * private key type matches the requested one or {@code null} if no match found
+ * @see #getPublicKeyType()
+ * @see #getPrivateKeyType()
+ */
+ public static final BuiltinIdentities fromKeyType(Class<?> clazz) {
+ if ((clazz == null) || (!Key.class.isAssignableFrom(clazz))) {
+ return null;
+ }
+
+ for (BuiltinIdentities id : VALUES) {
+ Class<?> pubType = id.getPublicKeyType(), prvType = id.getPrivateKeyType();
+ if (pubType.isAssignableFrom(clazz) || prvType.isAssignableFrom(clazz)) {
+ return id;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Contains the names of the identities
+ */
+ public static final class Constants {
+ public static final String RSA = "RSA";
+ public static final String DSA = "DSA";
+ public static final String ECDSA = "ECDSA";
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/common/config/keys/Identity.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/Identity.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/Identity.java
new file mode 100644
index 0000000..75b68be
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/Identity.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.config.keys;
+
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.OptionalFeature;
+
+/**
+ * Represents an SSH key type
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface Identity extends NamedResource, OptionalFeature {
+ /**
+ * @return The key algorithm - e.g., RSA, DSA, EC
+ */
+ String getAlgorithm();
+
+ Class<? extends PublicKey> getPublicKeyType();
+ Class<? extends PrivateKey> getPrivateKeyType();
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java
new file mode 100644
index 0000000..15e8c2f
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.config.keys;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.keyprovider.MappedKeyPairProvider;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.SecurityUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class IdentityUtils {
+ private IdentityUtils() {
+ throw new UnsupportedOperationException("No instance");
+ }
+
+ /**
+ * @param prefix The file name prefix - ignored if {@code null}/empty
+ * @param type The identity type - ignored if {@code null}/empty
+ * @param suffix The file name suffix - ignored if {@code null}/empty
+ * @return The identity file name or {@code null} if no name
+ */
+ public static String getIdentityFileName(String prefix, String type, String suffix) {
+ if (GenericUtils.isEmpty(type)) {
+ return null;
+ } else {
+ return new StringBuilder(GenericUtils.length(prefix) + type.length() + GenericUtils.length(suffix))
+ .append(GenericUtils.trimToEmpty(prefix))
+ .append(type.toLowerCase())
+ .append(GenericUtils.trimToEmpty(suffix))
+ .toString();
+ }
+ }
+
+ /**
+ * @param ids A {@link Map} of the loaded identities where key=the identity type,
+ * value=the matching {@link KeyPair} - ignored if {@code null}/empty
+ * @param supportedOnly If {@code true} then ignore identities that are not
+ * supported internally
+ * @return A {@link KeyPair} for the identities - {@code null} if no identities
+ * available (e.g., after filtering unsupported ones)
+ * @see BuiltinIdentities
+ */
+ public static KeyPairProvider createKeyPairProvider(Map<String,KeyPair> ids, boolean supportedOnly) {
+ if (GenericUtils.isEmpty(ids)) {
+ return null;
+ }
+
+ Map<String,KeyPair> pairsMap = new TreeMap<String, KeyPair>(String.CASE_INSENSITIVE_ORDER);
+ for (Map.Entry<String, KeyPair> ide : ids.entrySet()) {
+ String type = ide.getKey();
+ KeyPair kp = ide.getValue();
+ BuiltinIdentities id = BuiltinIdentities.fromName(type);
+ if (id == null) {
+ id = BuiltinIdentities.fromKeyPair(kp);
+ }
+
+ if (supportedOnly && ((id == null) || (!id.isSupported()))) {
+ continue;
+ }
+
+ String keyType = KeyUtils.getKeyType(kp);
+ if (GenericUtils.isEmpty(keyType)) {
+ continue;
+ }
+
+ KeyPair prev = pairsMap.put(keyType, kp);
+ if (prev != null) {
+ continue; // less of an offense if 2 pairs mapped to same key type
+ }
+ }
+
+ if (GenericUtils.isEmpty(pairsMap)) {
+ return null;
+ } else {
+ return new MappedKeyPairProvider(pairsMap);
+ }
+ }
+
+ /**
+ * @param paths A {@link Map} of the identities where key=identity type (case
+ * <U>insensitive</U>), value=the {@link Path} of file with the identity key
+ * @param provider A {@link FilePasswordProvider} - may be {@code null}
+ * if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
+ * to {@link FilePasswordProvider#getPassword(String)} is the path of the
+ * file whose key is to be loaded
+ * @param options The {@link OpenOption}s to use when reading the key data
+ * @return A {@link Map} of the identities where key=identity type (case
+ * <U>insensitive</U>), value=the {@link KeyPair} of the identity
+ * @throws IOException If failed to access the file system
+ * @throws GeneralSecurityException If failed to load the keys
+ * @see SecurityUtils#loadKeyPairIdentity(String, InputStream, FilePasswordProvider)
+ */
+ public static Map<String,KeyPair> loadIdentities(Map<String,? extends Path> paths, FilePasswordProvider provider, OpenOption ... options)
+ throws IOException, GeneralSecurityException {
+ if (GenericUtils.isEmpty(paths)) {
+ return Collections.emptyMap();
+ }
+
+ Map<String,KeyPair> ids = new TreeMap<String, KeyPair>(String.CASE_INSENSITIVE_ORDER);
+ for (Map.Entry<String,? extends Path> pe : paths.entrySet()) {
+ String type = pe.getKey();
+ Path path = pe.getValue();
+ try(InputStream inputStream = Files.newInputStream(path, options)) {
+ KeyPair kp = SecurityUtils.loadKeyPairIdentity(path.toString(), inputStream, provider);
+ KeyPair prev = ids.put(type, kp);
+ ValidateUtils.checkTrue(prev == null, "Multiple keys for type=%s", type);
+ }
+ }
+
+ return ids;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
index 2c37278..b285476 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
@@ -18,8 +18,13 @@
*/
package org.apache.sshd.common.config.keys;
+import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyPair;
@@ -39,9 +44,12 @@ import java.security.spec.ECParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
@@ -52,9 +60,11 @@ import org.apache.sshd.common.digest.Digest;
import org.apache.sshd.common.digest.DigestUtils;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.util.io.IoUtils;
/**
* Utility class for keys
@@ -73,11 +83,76 @@ public final class KeyUtils {
registerPublicKeyEntryDecoder(ECDSAPublicKeyEntryDecoder.INSTANCE);
}
+ /**
+ * The {@link Set} of {@link PosixFilePermission} <U>not</U> allowed if strict
+ * permissions are enforced on key files
+ */
+ public static final Set<PosixFilePermission> STRICTLY_PROHIBITED_FILE_PERMISSION =
+ Collections.unmodifiableSet(
+ EnumSet.of(PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE,
+ PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE));
+
private KeyUtils() {
throw new UnsupportedOperationException("No instance");
}
/**
+ * <P>Checks if a path has strict permissions</P></BR>
+ * <UL>
+ * <LI>
+ * The path may not have {@link PosixFilePermission#OTHERS_EXECUTE}
+ * permission
+ * </LI>
+ *
+ * <LI>
+ * (For {@code Unix}) The path may not have group or others permissions
+ * </LI>
+ *
+ * <LI>
+ * (For {@code Unix}) If the path is a file, then its folder may not have
+ * group or others permissions
+ * </LI>
+ * </UL>
+ * @param path The {@link Path} to be checked - ignored if {@code null}
+ * or does not exist
+ * @param options The {@link LinkOption}s to use to query the file's permissions
+ * @return The violated {@link PosixFilePermission} - {@code null} if
+ * no violations detected
+ * @throws IOException If failed to retrieve the permissions
+ * @see #STRICTLY_PROHIBITED_FILE_PERMISSION
+ */
+ public static PosixFilePermission validateStrictKeyFilePermissions(Path path, LinkOption ... options) throws IOException {
+ if ((path == null) || (!Files.exists(path, options))) {
+ return null;
+ }
+
+ Collection<PosixFilePermission> perms = IoUtils.getPermissions(path, options);
+ if (GenericUtils.isEmpty(perms)) {
+ return null;
+ }
+
+ if (perms.contains(PosixFilePermission.OTHERS_EXECUTE)) {
+ return PosixFilePermission.OTHERS_EXECUTE;
+ }
+
+ if (OsUtils.isUNIX()) {
+ PosixFilePermission p = IoUtils.validateExcludedPermissions(perms, STRICTLY_PROHIBITED_FILE_PERMISSION);
+ if (p != null) {
+ return p;
+ }
+
+ if (Files.isRegularFile(path, options)) {
+ Path parent=path.getParent();
+ if ((p = IoUtils.validateExcludedPermissions(IoUtils.getPermissions(parent, options), STRICTLY_PROHIBITED_FILE_PERMISSION)) != null) {
+ return p;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
* @param keyType The key type - {@code OpenSSH} name - e.g., {@code ssh-rsa, ssh-dss}
* @param keySize The key size (in bits)
* @return A {@link KeyPair} of the specified type and size
@@ -153,7 +228,28 @@ public final class KeyUtils {
return byKeyTypeDecodersMap.get(keyType);
}
}
-
+
+ /**
+ * @param kp The {@link KeyPair} to examine - ignored if {@code null}
+ * @return The matching {@link PublicKeyEntryDecoder} provided <U>both</U>
+ * the public and private keys have the same decoder - {@code null} if no
+ * match found
+ * @see #getPublicKeyEntryDecoder(Key)
+ */
+ public static PublicKeyEntryDecoder<?,?> getPublicKeyEntryDecoder(KeyPair kp) {
+ if (kp == null) {
+ return null;
+ }
+
+ PublicKeyEntryDecoder<?,?> d1 = getPublicKeyEntryDecoder(kp.getPublic());
+ PublicKeyEntryDecoder<?,?> d2 = getPublicKeyEntryDecoder(kp.getPrivate());
+ if (d1 == d2) {
+ return d1;
+ } else {
+ return null; // some kind of mixed keys...
+ }
+ }
+
/**
* @param key The {@link Key} (public or private) - ignored if {@code null}
* @return The registered {@link PublicKeyEntryDecoder} for this key or {code null} if no match found
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
index 45afa6e..1614985 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
@@ -577,7 +577,8 @@ public class ScpHelper extends AbstractLoggingBean {
readAck(false);
}
- Set<PosixFilePermission> perms = IoUtils.getPermissions(path);
+ LinkOption[] options = IoUtils.getLinkOptions(false);
+ Set<PosixFilePermission> perms = IoUtils.getPermissions(path, options);
StringBuilder buf = new StringBuilder();
buf.append("D");
buf.append(preserve ? getOctalPerms(perms) : "0755");
@@ -594,7 +595,6 @@ public class ScpHelper extends AbstractLoggingBean {
listener.startFolderEvent(FileOperation.SEND, path, perms);
try {
- LinkOption[] options = IoUtils.getLinkOptions(false);
for (Path child : children) {
if (Files.isRegularFile(child, options)) {
sendFile(child, preserve, bufferSize);
@@ -615,8 +615,8 @@ public class ScpHelper extends AbstractLoggingBean {
readAck(false);
}
- public static String getOctalPerms(Path path) throws IOException {
- return getOctalPerms(IoUtils.getPermissions(path));
+ public static String getOctalPerms(Path path, LinkOption ... options) throws IOException {
+ return getOctalPerms(IoUtils.getPermissions(path, options));
}
public static String getOctalPerms(Collection<PosixFilePermission> perms) {
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/common/util/SecurityUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/SecurityUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/SecurityUtils.java
index 134c9d6..5c6d6fe 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/SecurityUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/SecurityUtils.java
@@ -30,6 +30,7 @@ import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
+import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Signature;
import java.util.concurrent.Callable;
@@ -185,6 +186,28 @@ public class SecurityUtils {
}
}
+ /**
+ * @param resourceKey An identifier of the key being loaded - used as
+ * argument to the {@link FilePasswordProvider#getPassword(String)}
+ * invocation
+ * @param inputStream The {@link InputStream} for the <U>private</U> key
+ * @param provider A {@link FilePasswordProvider} - may be {@code null}
+ * if the loaded key is <U>guaranteed</U> not to be encrypted
+ * @return The loaded {@link KeyPair}
+ * @throws IOException If failed to read/parse the input stream
+ * @throws GeneralSecurityException If failed to generate the keys - specifically,
+ * {@link NoSuchProviderException} is thrown also if {@link #isBouncyCastleRegistered()}
+ * is {@code false}
+ */
+ public static KeyPair loadKeyPairIdentity(String resourceKey, InputStream inputStream, FilePasswordProvider provider)
+ throws IOException, GeneralSecurityException {
+ if (!isBouncyCastleRegistered()) {
+ throw new NoSuchProviderException("BouncyCastle not registered");
+ }
+
+ return BouncyCastleInputStreamLoader.loadKeyPair(resourceKey, inputStream, provider);
+ }
+
/* -------------------------------------------------------------------- */
// use a separate class in order to avoid direct dependency
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/common/util/io/IoUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/io/IoUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/io/IoUtils.java
index 73f7b59..7afc506 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/io/IoUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/io/IoUtils.java
@@ -37,6 +37,7 @@ import java.util.EnumSet;
import java.util.List;
import java.util.Set;
+import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.OsUtils;
/**
@@ -113,11 +114,11 @@ public class IoUtils {
* @throws IOException If failed to access the file system in order to
* retrieve the permissions
*/
- public static Set<PosixFilePermission> getPermissions(Path path) throws IOException {
+ public static Set<PosixFilePermission> getPermissions(Path path, LinkOption ... options) throws IOException {
FileSystem fs = path.getFileSystem();
Collection<String> views = fs.supportedFileAttributeViews();
if (views.contains("posix")) {
- return Files.getPosixFilePermissions(path, getLinkOptions(false));
+ return Files.getPosixFilePermissions(path, options);
} else {
return getPermissionsFromFile(path.toFile());
}
@@ -291,4 +292,24 @@ public class IoUtils {
return length;
}
+
+ /**
+ * @param perms The current {@link PosixFilePermission}s - ignored if {@code null}/empty
+ * @param excluded The permissions <U>not</U> allowed to exist - ignored if {@code null)/empty
+ * @return The violating {@link PosixFilePermission} - {@code null}
+ * if no violating permission found
+ */
+ public static PosixFilePermission validateExcludedPermissions(Collection<PosixFilePermission> perms, Collection<PosixFilePermission> excluded) {
+ if (GenericUtils.isEmpty(perms) || GenericUtils.isEmpty(excluded)) {
+ return null;
+ }
+
+ for (PosixFilePermission p : excluded) {
+ if (perms.contains(p)) {
+ return p;
+ }
+ }
+
+ return null;
+ }
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java
index ddecc16..ad2a2c8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java
@@ -26,12 +26,9 @@ import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.Set;
+import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.server.session.ServerSession;
@@ -44,14 +41,6 @@ import org.apache.sshd.server.session.ServerSession;
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class DefaultAuthorizedKeysAuthenticator extends AuthorizedKeysAuthenticator {
- /**
- * The {@link Set} of {@link PosixFilePermission} <U>not</U> allowed if strict
- * permissions are enforced
- */
- public static final Set<PosixFilePermission> STRICTLY_PROHIBITED_FILE_PERMISSION =
- Collections.unmodifiableSet(
- EnumSet.of(PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE,
- PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE));
/**
* The default instance that enforces the same permissions regime as {@code OpenSSH}
@@ -120,19 +109,8 @@ public class DefaultAuthorizedKeysAuthenticator extends AuthorizedKeysAuthentica
if (log.isDebugEnabled()) {
log.info("reloadAuthorizedKeys(" + username + ")[" + session + "] check permissions of " + path);
}
-
- Collection<PosixFilePermission> perms = IoUtils.getPermissions(path);
- // this is true for Windows as well
- if (perms.contains(PosixFilePermission.OTHERS_EXECUTE)) {
- throw new FileSystemException(path.toString(), path.toString(), "File is not allowed to have e(x)ecute permission");
- }
-
- if (OsUtils.isUNIX()) {
- validateFilePath(path, perms, STRICTLY_PROHIBITED_FILE_PERMISSION);
- Path parent=path.getParent();
- validateFilePath(parent, IoUtils.getPermissions(parent), STRICTLY_PROHIBITED_FILE_PERMISSION);
- }
+ KeyUtils.validateStrictKeyFilePermissions(path);
}
return super.reloadAuthorizedKeys(path, username, session);
@@ -146,14 +124,10 @@ public class DefaultAuthorizedKeysAuthenticator extends AuthorizedKeysAuthentica
* @throws IOException If an excluded permission appears in the current ones
*/
protected Path validateFilePath(Path path, Collection<PosixFilePermission> perms, Collection<PosixFilePermission> excluded) throws IOException {
- if (GenericUtils.isEmpty(perms) || GenericUtils.isEmpty(excluded)) {
- return path;
- }
-
- for (PosixFilePermission p : excluded) {
- if (perms.contains(p)) {
- throw new FileSystemException(path.toString(), path.toString(), "File is not allowed to have permission=" + p);
- }
+ PosixFilePermission p = IoUtils.validateExcludedPermissions(perms, excluded);
+ if (p != null) {
+ String filePath = path.toString();
+ throw new FileSystemException(filePath, filePath, "File not allowed to have " + p + " permission: " + filePath);
}
return path;
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/server/config/keys/ServerIdentity.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/ServerIdentity.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/ServerIdentity.java
new file mode 100644
index 0000000..898397c
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/ServerIdentity.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.server.config.keys;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.config.keys.IdentityUtils;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.SecurityUtils;
+import org.apache.sshd.common.util.Transformer;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.server.SshServer;
+
+/**
+ * Loads server identity key files - e.g., {@code /etc/ssh/ssh_host_rsa_key}
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see SecurityUtils#isBouncyCastleRegistered()
+ */
+public final class ServerIdentity {
+ public static final String ID_FILE_PREFIX = "ssh_host_", ID_FILE_SUFFIX = "_key";
+ /**
+ * The server's keys configuration multi-value
+ */
+ public static final String HOST_KEY_CONFIG_PROP="HostKey";
+
+ private ServerIdentity() {
+ throw new UnsupportedOperationException("No instance");
+ }
+
+ public static final Transformer<String,String> ID_GENERATOR =
+ new Transformer<String,String>() {
+ @Override
+ public String transform(String input) {
+ return getIdentityFileName(input);
+ }
+ };
+
+ /**
+ * Sets the server's {@link KeyPairProvider} with the loaded identities - if any
+ * @param server The {@link SshServer} to configure
+ * @param props The {@link Properties} holding the server's configuration - ignored
+ * if {@code null}/empty
+ * @param supportedOnly If {@code true} then ignore identities that are not
+ * supported internally
+ * @return The updated server
+ * @throws IOException If failed to access the file system
+ * @throws GeneralSecurityException If failed to load the keys
+ * @see #loadKeyPairProvider(Properties, boolean, LinkOption...)
+ */
+ public static <S extends SshServer> S setKeyPairProvider(S server, Properties props, boolean supportedOnly)
+ throws IOException, GeneralSecurityException {
+ KeyPairProvider provider = loadKeyPairProvider(props, supportedOnly, IoUtils.getLinkOptions(false));
+ if (provider != null) {
+ server.setKeyPairProvider(provider);
+ }
+
+ return server;
+ }
+
+ /**
+ * @param props The {@link Properties} holding the server's configuration - ignored
+ * if {@code null}/empty
+ * @param supportedOnly If {@code true} then ignore identities that are not
+ * supported internally
+ * @param options The {@link LinkOption}s to use when checking files existence
+ * @return A {@link KeyPair} for the identities - {@code null} if no identities
+ * available (e.g., after filtering unsupported ones)
+ * @throws IOException If failed to access the file system
+ * @throws GeneralSecurityException If failed to load the keys
+ * @see #loadIdentities(Properties, LinkOption...)
+ * @see IdentityUtils#createKeyPairProvider(Map, boolean)
+ */
+ public static KeyPairProvider loadKeyPairProvider(Properties props, boolean supportedOnly, LinkOption ... options)
+ throws IOException, GeneralSecurityException {
+ Map<String,KeyPair> ids = loadIdentities(props, options);
+ return IdentityUtils.createKeyPairProvider(ids, supportedOnly);
+ }
+
+ /**
+ * @param props The {@link Properties} holding the server's configuration - ignored
+ * if {@code null}/empty
+ * @param options The {@link LinkOption}s to use when checking files existence
+ * @return A {@link Map} of the identities where key=identity type (case
+ * <U>insensitive</U>), value=the {@link KeyPair} of the identity
+ * @throws IOException If failed to access the file system
+ * @throws GeneralSecurityException If failed to load the keys
+ * @see #findIdentities(Properties, LinkOption...)
+ * @see IdentityUtils#loadIdentities(Map, org.apache.sshd.common.config.keys.FilePasswordProvider, java.nio.file.OpenOption...)
+ */
+ public static Map<String,KeyPair> loadIdentities(Properties props, LinkOption ... options) throws IOException, GeneralSecurityException {
+ Map<String,Path> ids = findIdentities(props, options);
+ return IdentityUtils.loadIdentities(ids, null /* server key files are never encrypted */, IoUtils.EMPTY_OPEN_OPTIONS);
+ }
+
+ /**
+ * @param props The {@link Properties} holding the server's configuration - ignored
+ * if {@code null}/empty
+ * @param options The {@link LinkOption}s to use when checking files existence
+ * @return A {@link Map} of the found identities where key=the identity type
+ * (case <U>insensitive</I>) and value=the {@link Path} of the file holding
+ * the specific type key
+ * @throws IOException If failed to access the file system
+ * @see #getIdentityType(String)
+ * @see #HOST_KEY_CONFIG_PROP
+ * @see SshConfigFileReader#readConfigFile(File)
+ */
+ public static Map<String,Path> findIdentities(Properties props, LinkOption ... options) throws IOException {
+ if (GenericUtils.isEmpty(props)) {
+ return Collections.emptyMap();
+ }
+
+ String keyList = props.getProperty(HOST_KEY_CONFIG_PROP);
+ String[] paths = GenericUtils.split(keyList, ',');
+ if (GenericUtils.isEmpty(paths)) {
+ return Collections.emptyMap();
+ }
+
+ Map<String,Path> ids = new TreeMap<String, Path>(String.CASE_INSENSITIVE_ORDER);
+ for (String p : paths) {
+ File file = new File(p);
+ Path path = file.toPath();
+ if (!Files.exists(path, options)) {
+ continue;
+ }
+
+ String type = getIdentityType(path.getFileName().toString());
+ if (GenericUtils.isEmpty(type)) {
+ type = p; // just in case the file name does not adhere to the standard naming convention
+ }
+ Path prev = ids.put(type, path);
+ ValidateUtils.checkTrue(prev == null, "Multiple mappings for type=%s", type);
+ }
+
+ return ids;
+ }
+
+ /**
+ * @param name The file name - ignored if {@code null}/empty
+ * @return The identity type - {@code null} if cannot determine it - e.g.,
+ * does not start/end with the {@link #ID_FILE_PREFIX}/{@link #ID_FILE_SUFFIX}
+ */
+ public static String getIdentityType(String name) {
+ if (GenericUtils.isEmpty(name)
+ || (name.length() <= (ID_FILE_PREFIX.length() + ID_FILE_SUFFIX.length()))
+ || (!name.startsWith(ID_FILE_PREFIX))
+ || (!name.endsWith(ID_FILE_SUFFIX))) {
+ return null;
+ } else {
+ return name.substring(ID_FILE_PREFIX.length(), name.length() - ID_FILE_SUFFIX.length());
+ }
+ }
+
+ public static String getIdentityFileName(NamedResource r) {
+ return getIdentityFileName((r == null) ? null : r.getName());
+ }
+
+ /**
+ * @param type The identity type - e.g., {@code rsa} - ignored
+ * if {@code null}/empty
+ * @return The matching file name for the identity - {@code null}
+ * if no name
+ * @see #ID_FILE_PREFIX
+ * @see #ID_FILE_SUFFIX
+ * @see IdentityUtils#getIdentityFileName(String, String, String)
+ */
+ public static String getIdentityFileName(String type) {
+ return IdentityUtils.getIdentityFileName(ID_FILE_PREFIX, type, ID_FILE_SUFFIX);
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSession.java b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSession.java
index 0639c4b..d5b8364 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSession.java
@@ -128,7 +128,7 @@ public class ServerSession extends AbstractSession {
}
if (resolvedHostKeys == null) {
- resolvedHostKeys = new StringBuilder();
+ resolvedHostKeys = new StringBuilder(supported.size() * 16 /* ecdsa-sha2-xxxx */);
}
if (resolvedHostKeys.length() > 0) {
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/test/java/org/apache/sshd/client/config/keys/ClientIdentityTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/keys/ClientIdentityTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/keys/ClientIdentityTest.java
new file mode 100644
index 0000000..aa8d957
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/config/keys/ClientIdentityTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.config.keys;
+
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Map;
+
+import org.apache.sshd.common.config.keys.BuiltinIdentities;
+import org.apache.sshd.common.config.keys.IdentityUtils;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.SecurityUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.util.BaseTestSupport;
+import org.junit.Assume;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class ClientIdentityTest extends BaseTestSupport {
+ public ClientIdentityTest() {
+ super();
+ }
+
+ @Test
+ public void testLoadClientIdentities() throws Exception {
+ Assume.assumeTrue("BouncyCastle not registered", SecurityUtils.isBouncyCastleRegistered());
+
+ Path resFolder = getClassResourcesFolder(TEST_SUBFOLDER, getClass()).toPath();
+ LinkOption[] options = IoUtils.getLinkOptions(false);
+ Collection<BuiltinIdentities> expected = EnumSet.noneOf(BuiltinIdentities.class);
+ for (BuiltinIdentities type : BuiltinIdentities.VALUES) {
+ String fileName = ClientIdentity.getIdentityFileName(type);
+ Path file = resFolder.resolve(fileName);
+ if (!Files.exists(file, options)) {
+ System.out.println("Skip non-existing identity file " + file);
+ continue;
+ }
+
+ if (!type.isSupported()) {
+ System.out.println("Skip unsupported identity file " + file);
+ continue;
+ }
+
+ expected.add(type);
+ }
+
+ Map<String,KeyPair> ids = ClientIdentity.loadDefaultIdentities(
+ resFolder,
+ false, // don't be strict
+ null, // none of the files is password protected
+ options);
+ assertEquals("Mismatched loaded ids count", GenericUtils.size(expected), GenericUtils.size(ids));
+
+ Collection<KeyPair> pairs = new ArrayList<KeyPair>(ids.size());
+ for (BuiltinIdentities type : BuiltinIdentities.VALUES) {
+ if (expected.contains(type)) {
+ KeyPair kp = ids.get(type.getName());
+ assertNotNull("No key pair loaded for " + type, kp);
+ pairs.add(kp);
+ }
+ }
+
+ KeyPairProvider provider = IdentityUtils.createKeyPairProvider(ids, true /* supported only */);
+ assertNotNull("No provider generated", provider);
+
+ Iterable<KeyPair> keys = provider.loadKeys();
+ for (KeyPair kp : keys) {
+ assertTrue("Unexpected loaded key: " + kp, pairs.remove(kp));
+ }
+
+ assertEquals("Not all pairs listed", 0, pairs.size());
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/612a2f47/sshd-core/src/test/java/org/apache/sshd/common/config/keys/BuiltinIdentitiesTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/config/keys/BuiltinIdentitiesTest.java b/sshd-core/src/test/java/org/apache/sshd/common/config/keys/BuiltinIdentitiesTest.java
new file mode 100644
index 0000000..c0bb996
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/config/keys/BuiltinIdentitiesTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.config.keys;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+
+import org.apache.sshd.common.util.SecurityUtils;
+import org.apache.sshd.util.BaseTestSupport;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class BuiltinIdentitiesTest extends BaseTestSupport {
+ public BuiltinIdentitiesTest() {
+ super();
+ }
+
+ @Test
+ public void testFromName() {
+ for (BuiltinIdentities expected : BuiltinIdentities.VALUES) {
+ String name = expected.getName();
+ for (int index = 0; index < name.length(); index++) {
+ assertSame(name, expected, BuiltinIdentities.fromName(name));
+ name = shuffleCase(name);
+ }
+ }
+ }
+
+ @Test
+ public void testFromAlgorithm() {
+ for (BuiltinIdentities expected : BuiltinIdentities.VALUES) {
+ String algorithm = expected.getAlgorithm();
+ for (int index = 0; index < algorithm.length(); index++) {
+ assertSame(algorithm, expected, BuiltinIdentities.fromAlgorithm(algorithm));
+ algorithm = shuffleCase(algorithm);
+ }
+ }
+ }
+
+ @Test
+ public void testFromKey() throws GeneralSecurityException {
+ for (BuiltinIdentities expected : BuiltinIdentities.VALUES) {
+ String name = expected.getName();
+ if (!expected.isSupported()) {
+ System.out.println("Skip unsupported built-in identity: " + name);
+ continue;
+ }
+
+ KeyPairGenerator gen = SecurityUtils.getKeyPairGenerator(expected.getAlgorithm());
+ KeyPair kp = gen.generateKeyPair();
+ System.out.println("Checking built-in identity: " + name);
+ assertSame(name + "[pair]", expected, BuiltinIdentities.fromKeyPair(kp));
+ assertSame(name + "[public]", expected, BuiltinIdentities.fromKey(kp.getPublic()));
+ assertSame(name + "[private]", expected, BuiltinIdentities.fromKey(kp.getPrivate()));
+ }
+ }
+
+ @Test
+ public void testAllConstantsCovered() throws Exception {
+ Field[] fields = BuiltinIdentities.Constants.class.getFields();
+ for (Field f : fields) {
+ int mods = f.getModifiers();
+ if (!Modifier.isStatic(mods)) {
+ continue;
+ }
+
+ if (!Modifier.isFinal(mods)) {
+ continue;
+ }
+
+ Class<?> type = f.getType();
+ if (!String.class.isAssignableFrom(type)) {
+ continue;
+ }
+
+ String name = f.getName(), value = (String) f.get(null);
+ BuiltinIdentities id = BuiltinIdentities.fromName(value);
+ assertNotNull("No match found for field " + name + "=" + value, id);
+ }
+ }
+}