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 2018/11/19 12:50:17 UTC

[2/3] mina-sshd git commit: [SSHD-865] Use lazy-loading for keys specified in the HostConfigEntry

[SSHD-865] Use lazy-loading for keys specified in the HostConfigEntry


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/98a708a5
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/98a708a5
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/98a708a5

Branch: refs/heads/master
Commit: 98a708a555cd5ba123161c8cbaa627cb855dff65
Parents: e849cc5
Author: Lyor Goldstein <lg...@apache.org>
Authored: Sun Nov 18 17:32:30 2018 +0200
Committer: Lyor Goldstein <lg...@apache.org>
Committed: Mon Nov 19 14:50:10 2018 +0200

----------------------------------------------------------------------
 CHANGES.md                                      |   3 +
 .../config/keys/ClientIdentityFileWatcher.java  |  13 +-
 .../config/keys/ClientIdentityLoader.java       |  22 +++
 .../config/keys/ClientIdentityLoaderHolder.java |  34 ++++
 .../keys/ClientIdentityLoaderManager.java       |  29 ++++
 .../keys/LazyClientKeyIdentityProvider.java     | 157 +++++++++++++++++++
 .../config/keys/FilePasswordProviderHolder.java |  35 +++++
 .../keys/FilePasswordProviderManager.java       |   9 +-
 .../common/keyprovider/KeyIdentityProvider.java |  19 ++-
 .../KeyIdentityProviderResolutionTest.java      |  90 +++++++++++
 .../sshd/client/ClientFactoryManager.java       |  10 +-
 .../java/org/apache/sshd/client/SshClient.java  | 112 +++++--------
 .../hosts/HostConfigEntryResolverTest.java      |  10 +-
 13 files changed, 438 insertions(+), 105 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/CHANGES.md
----------------------------------------------------------------------
diff --git a/CHANGES.md b/CHANGES.md
index 3ca0d3d..9cbb99f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -60,6 +60,9 @@ accept also an `AttributeRepository` connection context argument (propagated fro
 * `SshClient` and `ClientSession` use a `KeyIdentityProvider` instead of a full blown `KeyPairProvider`.
 `KeyPairProvider` is used only in the context of an `SshServer` and/or `ServerSession`.
 
+* `SshClient#loadClientIdentities` has been renamed to `preloadClientIdentities` + it returns a
+`KeyIdentityProvider` instead of a collection of strings representing paths.
+
 ## Behavioral changes and enhancements
 
 * [SSHD-849](https://issues.apache.org/jira/browse/SSHD-849) - Data forwarding code makes sure all

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityFileWatcher.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityFileWatcher.java b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityFileWatcher.java
index ca81e47..5832911 100644
--- a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityFileWatcher.java
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityFileWatcher.java
@@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
 
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.FilePasswordProviderHolder;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.session.SessionContext;
 import org.apache.sshd.common.util.GenericUtils;
@@ -43,7 +44,9 @@ import org.apache.sshd.common.util.io.resource.PathResource;
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class ClientIdentityFileWatcher extends ModifiableFileWatcher implements ClientIdentityProvider {
+public class ClientIdentityFileWatcher
+        extends ModifiableFileWatcher
+        implements ClientIdentityProvider, ClientIdentityLoaderHolder, FilePasswordProviderHolder {
     private final AtomicReference<KeyPair> identityHolder = new AtomicReference<>(null);
     private final Supplier<? extends ClientIdentityLoader> loaderHolder;
     private final Supplier<? extends FilePasswordProvider> providerHolder;
@@ -73,15 +76,17 @@ public class ClientIdentityFileWatcher extends ModifiableFileWatcher implements
         this.strict = strict;
     }
 
-    public final boolean isStrict() {
+    public boolean isStrict() {
         return strict;
     }
 
-    public final ClientIdentityLoader getClientIdentityLoader() {
+    @Override
+    public ClientIdentityLoader getClientIdentityLoader() {
         return loaderHolder.get();
     }
 
-    public final FilePasswordProvider getFilePasswordProvider() {
+    @Override
+    public FilePasswordProvider getFilePasswordProvider() {
         return providerHolder.get();
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java
index 1d941ac..ceb914e 100644
--- a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java
@@ -26,11 +26,14 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
+import java.util.Collection;
 import java.util.Objects;
 
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
 import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.io.resource.PathResource;
@@ -105,4 +108,23 @@ public interface ClientIdentityLoader {
     KeyPair loadClientIdentity(
         SessionContext session, NamedResource location, FilePasswordProvider provider)
             throws IOException, GeneralSecurityException;
+
+    /**
+     * Uses the provided {@link ClientIdentityLoader} to <U>lazy</U> load the keys locations
+     *
+     * @param loader The loader instance to use
+     * @param locations The locations to load - ignored if {@code null}/empty
+     * @param passwordProvider The {@link FilePasswordProvider} to use if any
+     * encrypted keys found
+     * @param ignoreNonExisting Whether to ignore non existing locations as indicated
+     * by {@link #isValidLocation(NamedResource)}
+     * @return The {@link KeyIdentityProvider} wrapper
+     */
+    static KeyIdentityProvider asKeyIdentityProvider(
+            ClientIdentityLoader loader, Collection<? extends NamedResource> locations,
+            FilePasswordProvider passwordProvider, boolean ignoreNonExisting) {
+        return GenericUtils.isEmpty(locations)
+            ? KeyIdentityProvider.EMPTY_KEYS_PROVIDER
+            : new LazyClientKeyIdentityProvider(loader, locations, passwordProvider, ignoreNonExisting);
+    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoaderHolder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoaderHolder.java b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoaderHolder.java
new file mode 100644
index 0000000..ecd0f0e
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoaderHolder.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface ClientIdentityLoaderHolder {
+    /**
+     * @return The {@link ClientIdentityLoader} to use in order to load client
+     * key pair identities - never {@code null}
+     */
+    ClientIdentityLoader getClientIdentityLoader();
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoaderManager.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoaderManager.java b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoaderManager.java
new file mode 100644
index 0000000..9aa2e55
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoaderManager.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ClientIdentityLoaderManager extends ClientIdentityLoaderHolder {
+    void setClientIdentityLoader(ClientIdentityLoader loader);
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/sshd-common/src/main/java/org/apache/sshd/client/config/keys/LazyClientKeyIdentityProvider.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/keys/LazyClientKeyIdentityProvider.java b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/LazyClientKeyIdentityProvider.java
new file mode 100644
index 0000000..05596be
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/keys/LazyClientKeyIdentityProvider.java
@@ -0,0 +1,157 @@
+/*
+ * 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.FileNotFoundException;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.FilePasswordProviderHolder;
+import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
+import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class LazyClientKeyIdentityProvider implements KeyIdentityProvider, ClientIdentityLoaderHolder, FilePasswordProviderHolder {
+    private final ClientIdentityLoader clientIdentityLoader;
+    private final Collection<? extends NamedResource> locations;
+    private final FilePasswordProvider passwordProvider;
+    private final boolean ignoreNonExisting;
+
+    public LazyClientKeyIdentityProvider(
+            ClientIdentityLoader loader, Collection<? extends NamedResource> locations,
+            FilePasswordProvider passwordProvider, boolean ignoreNonExisting) {
+        this.clientIdentityLoader = Objects.requireNonNull(loader, "No client identity loader provided");
+        this.locations = locations;
+        this.passwordProvider = passwordProvider;
+        this.ignoreNonExisting = ignoreNonExisting;
+    }
+
+    @Override
+    public ClientIdentityLoader getClientIdentityLoader() {
+        return clientIdentityLoader;
+    }
+
+    public Collection<? extends NamedResource> getLocations() {
+        return locations;
+    }
+
+    @Override
+    public FilePasswordProvider getFilePasswordProvider() {
+        return passwordProvider;
+    }
+
+    public boolean isIgnoreNonExisting() {
+        return ignoreNonExisting;
+    }
+
+    @Override
+    public Iterable<KeyPair> loadKeys(SessionContext session)
+            throws IOException, GeneralSecurityException {
+        Collection<? extends NamedResource> locs = getLocations();
+        if (GenericUtils.isEmpty(locs)) {
+            return Collections.emptyList();
+        }
+
+        return () -> new Iterator<KeyPair>() {
+            private final Iterator<? extends NamedResource> iter = locs.iterator();
+            private KeyPair currentPair;
+            private boolean finished;
+
+            @Override
+            public boolean hasNext() {
+                if (finished) {
+                    return false;
+                }
+
+                while (iter.hasNext()) {
+                    NamedResource l = iter.next();
+                    try {
+                        currentPair = loadClientIdentity(session, l);
+                    } catch (IOException | GeneralSecurityException e) {
+                        throw new RuntimeException("Failed (" + e.getClass().getSimpleName() + ")"
+                            + " to load key from " + l.getName() + ": " + e.getMessage(), e);
+                    }
+
+                    if (currentPair != null) {
+                        return true;
+                    }
+                }
+
+                finished = true;
+                return false;
+            }
+
+            @Override
+            public KeyPair next() {
+                if (finished) {
+                    throw new NoSuchElementException("All identities have been exhausted");
+                }
+                if (currentPair == null) {
+                    throw new IllegalStateException("'next()' called without asking 'hasNext()'");
+                }
+
+                KeyPair kp = currentPair;
+                currentPair = null;
+                return kp;
+            }
+
+            @Override
+            public String toString() {
+                return Iterator.class.getSimpleName() + "[" + LazyClientKeyIdentityProvider.class.getSimpleName() + "]";
+            }
+        };
+    }
+
+    protected KeyPair loadClientIdentity(SessionContext session, NamedResource location)
+            throws IOException, GeneralSecurityException {
+        ClientIdentityLoader loader = getClientIdentityLoader();
+        boolean ignoreInvalid = isIgnoreNonExisting();
+        try {
+            if (!loader.isValidLocation(location)) {
+                if (ignoreInvalid) {
+                    return null;
+                }
+
+                throw new FileNotFoundException("Invalid identity location: " + location.getName());
+            }
+        } catch (IOException e) {
+            if (ignoreInvalid) {
+                return null;
+            }
+
+            throw e;
+        }
+
+        return loader.loadClientIdentity(session, location, getFilePasswordProvider());
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProviderHolder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProviderHolder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProviderHolder.java
new file mode 100644
index 0000000..a872fac
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProviderHolder.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface FilePasswordProviderHolder {
+    /**
+     * @return The {@link FilePasswordProvider} to use if need to load encrypted
+     * identities keys - never {@code null}
+     * @see FilePasswordProvider#EMPTY
+     */
+    FilePasswordProvider getFilePasswordProvider();
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProviderManager.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProviderManager.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProviderManager.java
index aae151c..e38443a 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProviderManager.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProviderManager.java
@@ -24,13 +24,6 @@ package org.apache.sshd.common.config.keys;
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public interface FilePasswordProviderManager {
-    /**
-     * @return The {@link FilePasswordProvider} to use if need to load encrypted
-     * identities keys - never {@code null}
-     * @see FilePasswordProvider#EMPTY
-     */
-    FilePasswordProvider getFilePasswordProvider();
-
+public interface FilePasswordProviderManager extends FilePasswordProviderHolder {
     void setFilePasswordProvider(FilePasswordProvider provider);
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyIdentityProvider.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyIdentityProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyIdentityProvider.java
index 3298f20..6ce5996 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyIdentityProvider.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyIdentityProvider.java
@@ -62,12 +62,20 @@ public interface KeyIdentityProvider {
     Iterable<KeyPair> loadKeys(SessionContext session) throws IOException, GeneralSecurityException;
 
     /**
+     * @param provider The {@link KeyIdentityProvider} instance to verify
+     * @return {@code true} if instance is {@code null} or the {@link #EMPTY_KEYS_PROVIDER}
+     */
+    static boolean isEmpty(KeyIdentityProvider provider) {
+        return (provider == null) || GenericUtils.isSameReference(provider, EMPTY_KEYS_PROVIDER);
+    }
+
+    /**
      * <P>Creates a &quot;unified&quot; {@link KeyIdentityProvider} out of 2 possible ones
      * as follows:</P></BR>
      * <UL>
      *      <LI>If both are {@code null} then return {@code null}.</LI>
-     *      <LI>If either one is {@code null} then use the non-{@code null} one.</LI>
-     *      <LI>If both are the same instance then use it.</U>
+     *      <LI>If either one is {@code null}/{@link #EMPTY_KEYS_PROVIDER empty} then use the non-{@code null} one.</LI>
+     *      <LI>If both are the same instance then use the instance.</U>
      *      <LI>Otherwise, returns a wrapper that groups both providers.</LI>
      * </UL>
      *
@@ -78,9 +86,10 @@ public interface KeyIdentityProvider {
      */
     static KeyIdentityProvider resolveKeyIdentityProvider(
             KeyIdentityProvider identities, KeyIdentityProvider keys) {
-        if ((keys == null) || (identities == keys)) {
-            return identities;
-        } else if (identities == null) {
+        if (isEmpty(keys) || GenericUtils.isSameReference(identities, keys)) {
+            // Prefer EMPTY over null
+            return (identities == null) ? keys : identities;
+        } else if (isEmpty(identities)) {
             return keys;
         } else {
             return multiProvider(identities, keys);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/sshd-common/src/test/java/org/apache/sshd/common/keyprovider/KeyIdentityProviderResolutionTest.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/keyprovider/KeyIdentityProviderResolutionTest.java b/sshd-common/src/test/java/org/apache/sshd/common/keyprovider/KeyIdentityProviderResolutionTest.java
new file mode 100644
index 0000000..15ebb5d
--- /dev/null
+++ b/sshd-common/src/test/java/org/apache/sshd/common/keyprovider/KeyIdentityProviderResolutionTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.keyprovider;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
+import org.apache.sshd.util.test.JUnitTestSupport;
+import org.apache.sshd.util.test.NoIoTestCase;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+import org.mockito.Mockito;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
+@Category({ NoIoTestCase.class })
+public class KeyIdentityProviderResolutionTest extends JUnitTestSupport {
+    private final KeyIdentityProvider p1;
+    private final KeyIdentityProvider p2;
+    private final KeyIdentityProvider expected;
+
+    public KeyIdentityProviderResolutionTest(
+            KeyIdentityProvider p1, KeyIdentityProvider p2, KeyIdentityProvider expected) {
+        this.p1 = p1;
+        this.p2 = p2;
+        this.expected = expected;
+    }
+
+    @Parameters(name = "p1={0}, p2={1}, expected={2}")
+    public static List<Object[]> parameters() {
+        return new ArrayList<Object[]>() {
+            // Not serializing it
+            private static final long serialVersionUID = 1L;
+
+            {
+                add(new Object[]{null, null, null});
+                add(new Object[]{null, KeyIdentityProvider.EMPTY_KEYS_PROVIDER, KeyIdentityProvider.EMPTY_KEYS_PROVIDER});
+                add(new Object[]{KeyIdentityProvider.EMPTY_KEYS_PROVIDER, null, KeyIdentityProvider.EMPTY_KEYS_PROVIDER});
+                add(new Object[]{KeyIdentityProvider.EMPTY_KEYS_PROVIDER, KeyIdentityProvider.EMPTY_KEYS_PROVIDER, KeyIdentityProvider.EMPTY_KEYS_PROVIDER});
+
+                KeyIdentityProvider p = createKeyIdentityProvider("MOCK");
+                add(new Object[]{null, p, p});
+                add(new Object[]{KeyIdentityProvider.EMPTY_KEYS_PROVIDER, p, p});
+                add(new Object[]{p, null, p});
+                add(new Object[]{p, KeyIdentityProvider.EMPTY_KEYS_PROVIDER, p});
+            }
+
+            private KeyIdentityProvider createKeyIdentityProvider(String name) {
+                KeyIdentityProvider p = Mockito.mock(KeyIdentityProvider.class);
+                Mockito.when(p.toString()).thenReturn(name);
+                return p;
+            }
+        };
+    }
+
+    @Test
+    public void testResolveKeyIdentityProvider() {
+        assertSame(expected, KeyIdentityProvider.resolveKeyIdentityProvider(p1, p2));
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
index 35b42b1..368ebc2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
@@ -20,6 +20,7 @@ package org.apache.sshd.client;
 
 import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
 import org.apache.sshd.client.config.keys.ClientIdentityLoader;
+import org.apache.sshd.client.config.keys.ClientIdentityLoaderManager;
 import org.apache.sshd.client.session.ClientProxyConnectorHolder;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.config.keys.FilePasswordProviderManager;
@@ -34,6 +35,7 @@ public interface ClientFactoryManager
         extends FactoryManager,
                 ClientProxyConnectorHolder,
                 FilePasswordProviderManager,
+                ClientIdentityLoaderManager,
                 ClientAuthenticationManager {
 
     /**
@@ -84,12 +86,4 @@ public interface ClientFactoryManager
     HostConfigEntryResolver getHostConfigEntryResolver();
 
     void setHostConfigEntryResolver(HostConfigEntryResolver resolver);
-
-    /**
-     * @return The {@link ClientIdentityLoader} to use in order to load client
-     * key pair identities - never {@code null}
-     */
-    ClientIdentityLoader getClientIdentityLoader();
-
-    void setClientIdentityLoader(ClientIdentityLoader loader);
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/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 796d781..1dc6859 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
@@ -18,9 +18,7 @@
  */
 package org.apache.sshd.client;
 
-import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.StreamCorruptedException;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.net.SocketTimeoutException;
@@ -501,7 +499,7 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
                     log.debug("connect({}@{}:{}) no overrides", username, host, port);
                 }
 
-                return doConnect(username, targetAddress, context, localAddress, Collections.emptyList(), true);
+                return doConnect(username, targetAddress, context, localAddress, KeyIdentityProvider.EMPTY_KEYS_PROVIDER, true);
             } else {
                 if (log.isDebugEnabled()) {
                     log.debug("connect({}@{}:{}) effective: {}", username, host, port, entry);
@@ -513,7 +511,7 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
             if (log.isDebugEnabled()) {
                 log.debug("connect({}@{}) not an InetSocketAddress: {}", username, targetAddress, targetAddress.getClass().getName());
             }
-            return doConnect(username, targetAddress, context, localAddress, Collections.emptyList(), true);
+            return doConnect(username, targetAddress, context, localAddress, KeyIdentityProvider.EMPTY_KEYS_PROVIDER, true);
         }
     }
 
@@ -530,60 +528,27 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
         Collection<PathResource> idFiles = GenericUtils.isEmpty(hostIds)
             ? Collections.emptyList()
             : hostIds.stream()
-                .map(l -> Paths.get(l))
+                .map(Paths::get)
                 .map(PathResource::new)
                 .collect(Collectors.toCollection(() -> new ArrayList<>(hostIds.size())));
-        Collection<KeyPair> keys = loadClientIdentities(idFiles);
+        KeyIdentityProvider keys = preloadClientIdentities(idFiles);
         return doConnect(hostConfig.getUsername(), new InetSocketAddress(host, port),
                 context, localAddress, keys, !hostConfig.isIdentitiesOnly());
     }
 
-    protected List<KeyPair> loadClientIdentities(Collection<? extends NamedResource> locations) throws IOException {
-        if (GenericUtils.isEmpty(locations)) {
-            return Collections.emptyList();
-        }
-
-        List<KeyPair> ids = new ArrayList<>(locations.size());
-        boolean ignoreNonExisting = this.getBooleanProperty(IGNORE_INVALID_IDENTITIES, DEFAULT_IGNORE_INVALID_IDENTITIES);
-        ClientIdentityLoader loader = Objects.requireNonNull(getClientIdentityLoader(), "No ClientIdentityLoader");
-        FilePasswordProvider provider = getFilePasswordProvider();
-        boolean debugEnabled = log.isDebugEnabled();
-        for (NamedResource l : locations) {
-            if (!loader.isValidLocation(l)) {
-                if (ignoreNonExisting) {
-                    if (debugEnabled) {
-                        log.debug("loadClientIdentities - skip non-existing identity location: {}", l);
-                    }
-                    continue;
-                }
-
-                throw new FileNotFoundException("Invalid identity location: " + l);
-            }
-
-            try {
-                KeyPair kp = loader.loadClientIdentity(null /* TODO use lazy-load here as well */, l, provider);
-                if (kp == null) {
-                    throw new IOException("No identity loaded from " + l);
-                }
-
-                if (debugEnabled) {
-                    log.debug("loadClientIdentities({}) type={}, fingerprint={}",
-                          l, KeyUtils.getKeyType(kp), KeyUtils.getFingerPrint(kp.getPublic()));
-                }
-
-                ids.add(kp);
-            } catch (GeneralSecurityException e) {
-                throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ") to load identity from " + l + ": " + e.getMessage());
-            }
-        }
-
-        return ids;
+    protected KeyIdentityProvider preloadClientIdentities(Collection<? extends NamedResource> locations) throws IOException {
+        return GenericUtils.isEmpty(locations)
+             ? KeyIdentityProvider.EMPTY_KEYS_PROVIDER
+             : ClientIdentityLoader.asKeyIdentityProvider(
+                     Objects.requireNonNull(getClientIdentityLoader(), "No ClientIdentityLoader"),
+                     locations, getFilePasswordProvider(),
+                     this.getBooleanProperty(IGNORE_INVALID_IDENTITIES, DEFAULT_IGNORE_INVALID_IDENTITIES));
     }
 
     protected ConnectFuture doConnect(
             String username, SocketAddress targetAddress,
             AttributeRepository context, SocketAddress localAddress,
-            Iterable<? extends KeyPair> identities, boolean useDefaultIdentities)
+            KeyIdentityProvider identities, boolean useDefaultIdentities)
                 throws IOException {
         if (connector == null) {
             throw new IllegalStateException("SshClient not started. Please call start() method before connecting to a server");
@@ -600,7 +565,7 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
 
     protected SshFutureListener<IoConnectFuture> createConnectCompletionListener(
             ConnectFuture connectFuture, String username, SocketAddress address,
-            Iterable<? extends KeyPair> identities, boolean useDefaultIdentities) {
+            KeyIdentityProvider identities, boolean useDefaultIdentities) {
         return new SshFutureListener<IoConnectFuture>() {
             @Override
             @SuppressWarnings("synthetic-access")
@@ -643,53 +608,46 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
 
     protected void onConnectOperationComplete(
             IoSession ioSession, ConnectFuture connectFuture,  String username,
-            SocketAddress address, Iterable<? extends KeyPair> identities, boolean useDefaultIdentities) {
+            SocketAddress address, KeyIdentityProvider identities, boolean useDefaultIdentities) {
         AbstractClientSession session = (AbstractClientSession) AbstractSession.getSession(ioSession);
         session.setUsername(username);
         session.setConnectAddress(address);
 
         if (useDefaultIdentities) {
-            setupDefaultSessionIdentities(session);
-        }
-
-        if (identities != null) {
-            boolean traceEnabled = log.isTraceEnabled();
-            for (KeyPair kp : identities) {
-                if (traceEnabled) {
-                    log.trace("onConnectOperationComplete({}) add identity type={}, fingerprint={}",
-                        session, KeyUtils.getKeyType(kp), KeyUtils.getFingerPrint(kp.getPublic()));
-                }
-                session.addPublicKeyIdentity(kp);
-            }
+            setupDefaultSessionIdentities(session, identities);
+        } else {
+            session.setKeyIdentityProvider((identities == null) ? KeyIdentityProvider.EMPTY_KEYS_PROVIDER : identities);
         }
 
         connectFuture.setSession(session);
     }
 
-    protected void setupDefaultSessionIdentities(ClientSession session) {
+    protected void setupDefaultSessionIdentities(ClientSession session, KeyIdentityProvider extraIdentities) {
+        boolean debugEnabled = log.isDebugEnabled();
         // check if session listener intervened
         KeyIdentityProvider kpSession = session.getKeyIdentityProvider();
         KeyIdentityProvider kpClient = getKeyIdentityProvider();
-        boolean debugEnabled = log.isDebugEnabled();
-        if (kpSession == null) {
-            session.setKeyIdentityProvider(kpClient);
-        } else {
-            if (kpSession != kpClient) {
-                if (debugEnabled) {
-                    log.debug("setupDefaultSessionIdentities({}) key identity provider override", session);
-                }
+        if (GenericUtils.isSameReference(kpSession, kpClient)) {
+            if (debugEnabled) {
+                log.debug("setupDefaultSessionIdentities({}) key identity provider override in session listener", session);
+            }
+        }
+
+        // Prefer the extra identities to come first since they were probably indicate by the host-config entry
+        KeyIdentityProvider kpEffective =
+            KeyIdentityProvider.resolveKeyIdentityProvider(extraIdentities, kpSession);
+        if (!GenericUtils.isSameReference(kpSession, kpEffective)) {
+            if (debugEnabled) {
+                log.debug("setupDefaultSessionIdentities({}) key identity provider enhanced", session);
             }
+            session.setKeyIdentityProvider(kpEffective);
         }
 
         PasswordIdentityProvider passSession = session.getPasswordIdentityProvider();
         PasswordIdentityProvider passClient = getPasswordIdentityProvider();
-        if (passSession == null) {
-            session.setPasswordIdentityProvider(passClient);
-        } else {
-            if (passSession != passClient) {
-                if (debugEnabled) {
-                    log.debug("setupDefaultSessionIdentities({}) password provider override", session);
-                }
+        if (!GenericUtils.isSameReference(passSession, passClient)) {
+            if (debugEnabled) {
+                log.debug("setupDefaultSessionIdentities({}) password provider override", session);
             }
         }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/98a708a5/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
index b76e46c..8ede710 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
@@ -32,6 +32,7 @@ import java.util.Collections;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.sshd.client.ClientFactoryManager;
 import org.apache.sshd.client.SshClient;
@@ -214,6 +215,7 @@ public class HostConfigEntryResolverTest extends BaseTestSupport {
         entry.addIdentity(clientIdentity);
         entry.setIdentitiesOnly(true);
 
+        AtomicInteger specificIdentityLoadCount = new AtomicInteger(0);
         client.setClientIdentityLoader(new ClientIdentityLoader() {
             @Override
             public boolean isValidLocation(NamedResource location) throws IOException {
@@ -225,6 +227,7 @@ public class HostConfigEntryResolverTest extends BaseTestSupport {
                     SessionContext session, NamedResource location, FilePasswordProvider provider)
                         throws IOException, GeneralSecurityException {
                 if (isValidLocation(location)) {
+                    specificIdentityLoadCount.incrementAndGet();
                     return specificIdentity;
                 }
 
@@ -243,12 +246,13 @@ public class HostConfigEntryResolverTest extends BaseTestSupport {
         client.setKeyIdentityProvider(provider);
 
         client.start();
-        try (ClientSession session = client.connect(entry).verify(7L, TimeUnit.SECONDS).getSession()) {
-            assertSame("Unexpected session key pairs provider", provider, session.getKeyIdentityProvider());
+        try (ClientSession session = client.connect(entry)
+                .verify(7L, TimeUnit.SECONDS)
+                .getSession()) {
             session.auth().verify(5L, TimeUnit.SECONDS);
             assertFalse("Unexpected default client identity attempted", defaultClientIdentityAttempted.get());
             assertNull("Default client identity auto-added", session.removePublicKeyIdentity(defaultIdentity));
-            assertNotNull("Entry identity not automatically added", session.removePublicKeyIdentity(specificIdentity));
+            assertEquals("Entry identity not used", 1, specificIdentityLoadCount.get());
             assertEffectiveRemoteAddress(session, entry);
         } finally {
             client.stop();