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 2022/02/17 15:46:50 UTC

[mina-sshd] branch master updated (98b89bd -> 8ea60df)

This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git.


    from 98b89bd  Fixed typo in CHANGES.md
     new 7fefebb  [SSHD-1246] Moved user HOME directory detection and usage to PathUtils
     new 8ea60df  [SSHD-1246] Added SshKeyDumpMain utility

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 CHANGES.md                                         |   7 +-
 docs/client-setup.md                               |  53 ++-
 sshd-cli/pom.xml                                   |  13 +-
 .../java/org/apache/sshd/cli/SshKeyDumpMain.java   | 408 +++++++++++++++++++++
 .../sshd/client/config/hosts/HostConfigEntry.java  |  32 +-
 .../sshd/common/config/keys/IdentityUtils.java     |  20 -
 .../sshd/common/config/keys/PublicKeyEntry.java    |   8 +-
 .../org/apache/sshd/common/util/io/PathUtils.java  |  73 ++++
 .../OpenSSHKeyPairResourceParserTestSupport.java   |   3 +-
 .../apache/sshd/common/util/io/PathUtilsTest.java  |  77 ++++
 .../GenerateOpenSSHClientCertificateTest.java      |   6 +-
 ...GenerateOpenSshClientCertificateOracleTest.java |   4 +-
 .../certificates/OpenSSHCertificateParserTest.java |   2 +-
 .../ClientOpenSSHCertificatesTest.java             |  13 +-
 .../common/config/keys/AuthorizedKeyEntryTest.java |   2 +-
 .../FileHostKeyCertificateProviderTest.java        |   3 +-
 .../common/signature/OpenSSHCertificateTest.java   |   5 +-
 .../java/org/apache/sshd/openpgp/PGPUtils.java     |   4 +-
 18 files changed, 656 insertions(+), 77 deletions(-)
 create mode 100644 sshd-cli/src/test/java/org/apache/sshd/cli/SshKeyDumpMain.java
 create mode 100644 sshd-common/src/test/java/org/apache/sshd/common/util/io/PathUtilsTest.java

[mina-sshd] 01/02: [SSHD-1246] Moved user HOME directory detection and usage to PathUtils

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 7fefebb5356d02fb52965e1686b8497ade94fb53
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Feb 17 08:41:47 2022 +0200

    [SSHD-1246] Moved user HOME directory detection and usage to PathUtils
---
 CHANGES.md                                         |  4 ++
 .../sshd/client/config/hosts/HostConfigEntry.java  | 32 ++-------
 .../sshd/common/config/keys/IdentityUtils.java     | 20 ------
 .../sshd/common/config/keys/PublicKeyEntry.java    |  3 +-
 .../org/apache/sshd/common/util/io/PathUtils.java  | 73 ++++++++++++++++++++
 .../apache/sshd/common/util/io/PathUtilsTest.java  | 78 ++++++++++++++++++++++
 .../java/org/apache/sshd/openpgp/PGPUtils.java     |  4 +-
 7 files changed, 163 insertions(+), 51 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 4571d35..894d234 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -57,6 +57,10 @@ to make sure the requested value does not exceed `Integer#MAX_VALUE` (sometimes
 or malformed packets. It is important to bear in mind that in the vast majority of the cases we do not want to be able to allocate arrays
 or lists having billions of elements as it would almost definitely cause out-of-memory issues.
 
+## User HOME directory resolution and usage have been moved to *PathUtils*
+
+Was originally in *HostConfigEntry*.
+
 ## Minor code helpers
 
 ## Behavioral changes and enhancements
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
index 31a62c2..5f4d33d 100644
--- a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
@@ -48,7 +48,6 @@ import java.util.TreeMap;
 
 import org.apache.sshd.common.auth.MutableUserHolder;
 import org.apache.sshd.common.config.ConfigFileReaderSupport;
-import org.apache.sshd.common.config.keys.IdentityUtils;
 import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.MapEntryUtils;
@@ -56,6 +55,7 @@ import org.apache.sshd.common.util.MapEntryUtils.NavigableMapBuilder;
 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.common.util.io.PathUtils;
 import org.apache.sshd.common.util.io.input.NoCloseInputStream;
 import org.apache.sshd.common.util.io.input.NoCloseReader;
 import org.apache.sshd.common.util.io.output.NoCloseOutputStream;
@@ -114,7 +114,6 @@ public class HostConfigEntry extends HostPatternsHolder implements MutableUserHo
 
     public static final String MULTI_VALUE_SEPARATORS = " ,";
 
-    public static final char HOME_TILDE_CHAR = '~';
     public static final char PATH_MACRO_CHAR = '%';
     public static final char LOCAL_HOME_MACRO = 'd';
     public static final char LOCAL_USER_MACRO = 'u';
@@ -1148,9 +1147,9 @@ public class HostConfigEntry extends HostPatternsHolder implements MutableUserHo
 
             for (int curPos = 0; curPos < elem.length(); curPos++) {
                 char ch = elem.charAt(curPos);
-                if (ch == HOME_TILDE_CHAR) {
+                if (ch == PathUtils.HOME_TILDE_CHAR) {
                     ValidateUtils.checkTrue((curPos == 0) && (index == 0), "Home tilde must be first: %s", id);
-                    appendUserHome(sb);
+                    PathUtils.appendUserHome(sb);
                 } else if (ch == PATH_MACRO_CHAR) {
                     curPos++;
                     ValidateUtils.checkTrue(curPos < elem.length(), "Missing macro modifier in %s", id);
@@ -1161,7 +1160,7 @@ public class HostConfigEntry extends HostPatternsHolder implements MutableUserHo
                             break;
                         case LOCAL_HOME_MACRO:
                             ValidateUtils.checkTrue((curPos == 1) && (index == 0), "Home macro must be first: %s", id);
-                            appendUserHome(sb);
+                            PathUtils.appendUserHome(sb);
                             break;
                         case LOCAL_USER_MACRO:
                             sb.append(ValidateUtils.checkNotNullAndNotEmpty(OsUtils.getCurrentUser(),
@@ -1194,29 +1193,6 @@ public class HostConfigEntry extends HostPatternsHolder implements MutableUserHo
         return sb.toString();
     }
 
-    public static StringBuilder appendUserHome(StringBuilder sb) {
-        return appendUserHome(sb, IdentityUtils.getUserHomeFolder());
-    }
-
-    public static StringBuilder appendUserHome(StringBuilder sb, Path userHome) {
-        return appendUserHome(sb, Objects.requireNonNull(userHome, "No user home folder").toString());
-    }
-
-    public static StringBuilder appendUserHome(StringBuilder sb, String userHome) {
-        if (GenericUtils.isEmpty(userHome)) {
-            return sb;
-        }
-
-        sb.append(userHome);
-        // strip any ending separator since we add our own
-        int len = sb.length();
-        if (sb.charAt(len - 1) == File.separatorChar) {
-            sb.setLength(len - 1);
-        }
-
-        return sb;
-    }
-
     /**
      * @return The default {@link Path} location of the OpenSSH hosts entries configuration file
      */
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java
index b353dbc..f727165 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java
@@ -23,7 +23,6 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.util.Collections;
@@ -48,25 +47,6 @@ public final class IdentityUtils {
         throw new UnsupportedOperationException("No instance");
     }
 
-    private static final class LazyDefaultUserHomeFolderHolder {
-        private static final Path PATH
-                = Paths.get(ValidateUtils.checkNotNullAndNotEmpty(System.getProperty("user.home"), "No user home"))
-                        .toAbsolutePath()
-                        .normalize();
-
-        private LazyDefaultUserHomeFolderHolder() {
-            throw new UnsupportedOperationException("No instance allowed");
-        }
-    }
-
-    /**
-     * @return The {@link Path} to the currently running user home
-     */
-    @SuppressWarnings("synthetic-access")
-    public static Path getUserHomeFolder() {
-        return LazyDefaultUserHomeFolderHolder.PATH;
-    }
-
     /**
      * @param  prefix The file name prefix - ignored if {@code null}/empty
      * @param  type   The identity type - ignored if {@code null}/empty
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java
index 8dc634c..882daa7 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java
@@ -42,6 +42,7 @@ import org.apache.sshd.common.session.SessionContext;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.NumberUtils;
 import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.PathUtils;
 
 /**
  * <P>
@@ -498,7 +499,7 @@ public class PublicKeyEntry implements Serializable, KeyTypeIndicator {
     }
 
     private static final class LazyDefaultKeysFolderHolder {
-        private static final Path PATH = IdentityUtils.getUserHomeFolder().resolve(STD_KEYFILE_FOLDER_NAME);
+        private static final Path PATH = PathUtils.getUserHomeFolder().resolve(STD_KEYFILE_FOLDER_NAME);
 
         private LazyDefaultKeysFolderHolder() {
             throw new UnsupportedOperationException("No instance allowed");
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java
index 52fec23..cd9ab9c 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java
@@ -19,11 +19,14 @@
 
 package org.apache.sshd.common.util.io;
 
+import java.io.File;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Comparator;
 import java.util.Objects;
 
 import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.functors.UnaryEquator;
 
 /**
@@ -43,6 +46,19 @@ public final class PathUtils {
     public static final UnaryEquator<Path> EQ_CASE_SENSITIVE_FILENAME
             = (p1, p2) -> BY_CASE_SENSITIVE_FILENAME.compare(p1, p2) == 0;
 
+    private static final class LazyDefaultUserHomeFolderHolder {
+        private static final Path PATH
+                = Paths.get(ValidateUtils.checkNotNullAndNotEmpty(System.getProperty("user.home"), "No user home"))
+                        .toAbsolutePath()
+                        .normalize();
+
+        private LazyDefaultUserHomeFolderHolder() {
+            throw new UnsupportedOperationException("No instance allowed");
+        }
+    }
+
+    public static final char HOME_TILDE_CHAR = '~';
+
     /**
      * Private Constructor
      */
@@ -73,4 +89,61 @@ public final class PathUtils {
         String n2 = Objects.toString(p2.getFileName(), null);
         return GenericUtils.safeCompare(n1, n2, caseSensitive);
     }
+
+    /**
+     * <UL>
+     *      <LI>Replaces <U>leading</U> '~' with user's HOME directory</LI>
+     *      <LI>Replaces any forward slashes with the O/S directory separator</LI>
+     * </UL>
+     *
+     * @param path Input path - ignored if {@code null}/empty/blank
+     * @return Adjusted path
+     */
+    public static String normalizePath(String path) {
+        if (GenericUtils.isBlank(path)) {
+            return path;
+        }
+
+        if (path.charAt(0) == HOME_TILDE_CHAR) {
+            Path homeDir = Objects.requireNonNull(getUserHomeFolder(), "No user home folder available");
+            if (path.length() > 1) {
+                path = homeDir + path.substring(1);
+            } else {
+                path = homeDir.toString();
+            }
+        }
+
+        return path.replace('/', File.separatorChar);
+    }
+
+    /**
+     * @return The {@link Path} to the currently running user home
+     */
+    @SuppressWarnings("synthetic-access")
+    public static Path getUserHomeFolder() {
+        return LazyDefaultUserHomeFolderHolder.PATH;
+    }
+
+    public static StringBuilder appendUserHome(StringBuilder sb) {
+        return appendUserHome(sb, getUserHomeFolder());
+    }
+
+    public static StringBuilder appendUserHome(StringBuilder sb, Path userHome) {
+        return appendUserHome(sb, Objects.requireNonNull(userHome, "No user home folder").toString());
+    }
+
+    public static StringBuilder appendUserHome(StringBuilder sb, String userHome) {
+        if (GenericUtils.isEmpty(userHome)) {
+            return sb;
+        }
+
+        sb.append(userHome);
+        // strip any ending separator since we add our own
+        int len = sb.length();
+        if (sb.charAt(len - 1) == File.separatorChar) {
+            sb.setLength(len - 1);
+        }
+
+        return sb;
+    }
 }
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/util/io/PathUtilsTest.java b/sshd-common/src/test/java/org/apache/sshd/common/util/io/PathUtilsTest.java
new file mode 100644
index 0000000..32c0480
--- /dev/null
+++ b/sshd-common/src/test/java/org/apache/sshd/common/util/io/PathUtilsTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.util.io;
+
+import java.io.File;
+import java.nio.file.Path;
+
+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.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Category({ NoIoTestCase.class })
+public class PathUtilsTest extends JUnitTestSupport {
+    public PathUtilsTest() {
+        super();
+    }
+
+    @Test
+    public void testNormalizeUserHomeOnlyPath() {
+        Path expected = PathUtils.getUserHomeFolder();
+        String actual = PathUtils.normalizePath(Character.toString(PathUtils.HOME_TILDE_CHAR));
+        assertEquals(expected.toString(), actual);
+    }
+
+    @Test
+    public void testNormalizeLeadingUserHomePath() {
+        Path expected = PathUtils.getUserHomeFolder()
+                .resolve(getClass().getSimpleName())
+                .resolve(getCurrentTestName())
+                ;
+        String actual = PathUtils.normalizePath(PathUtils.HOME_TILDE_CHAR
+            + File.separator + getClass().getSimpleName()
+            + File.separator + getCurrentTestName());
+        assertEquals(expected.toString(), actual);
+    }
+
+    @Test
+    public void testNormalizeStandardPath() {
+        String expected = detectTargetFolder().toString();
+        String actual = PathUtils.normalizePath(expected);
+        assertSame(expected, actual);
+    }
+
+    @Test
+    public void testNormalizeForwardSlash() {
+        String expected = detectTargetFolder().toString();
+        String actual = PathUtils.normalizePath(expected.replace(File.separatorChar, '/'));
+        if (File.separatorChar == '/') {
+            assertSame(expected, actual);
+        } else {
+            assertEquals(expected, actual);
+        }
+    }
+}
diff --git a/sshd-openpgp/src/main/java/org/apache/sshd/openpgp/PGPUtils.java b/sshd-openpgp/src/main/java/org/apache/sshd/openpgp/PGPUtils.java
index d061b6e..9159d79 100644
--- a/sshd-openpgp/src/main/java/org/apache/sshd/openpgp/PGPUtils.java
+++ b/sshd-openpgp/src/main/java/org/apache/sshd/openpgp/PGPUtils.java
@@ -28,10 +28,10 @@ import java.util.Set;
 import java.util.TreeMap;
 
 import org.apache.sshd.common.PropertyResolverUtils;
-import org.apache.sshd.common.config.keys.IdentityUtils;
 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.PathUtils;
 import org.c02e.jpgpj.CompressionAlgorithm;
 import org.c02e.jpgpj.EncryptionAlgorithm;
 import org.c02e.jpgpj.Key;
@@ -161,7 +161,7 @@ public final class PGPUtils {
     }
 
     private static final class LazyDefaultPgpKeysFolderHolder {
-        private static final Path PATH = IdentityUtils.getUserHomeFolder()
+        private static final Path PATH = PathUtils.getUserHomeFolder()
                 .resolve(OsUtils.isUNIX() ? STD_LINUX_PGP_FOLDER_NAME : STD_WINDOWS_PGP_FOLDER_NAME);
 
         private LazyDefaultPgpKeysFolderHolder() {

[mina-sshd] 02/02: [SSHD-1246] Added SshKeyDumpMain utility

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 8ea60dfa27b828f3122ff33dd14717eea1d30b45
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Feb 17 10:43:16 2022 +0200

    [SSHD-1246] Added SshKeyDumpMain utility
---
 CHANGES.md                                         |   3 +-
 docs/client-setup.md                               |  53 ++-
 sshd-cli/pom.xml                                   |  13 +-
 .../java/org/apache/sshd/cli/SshKeyDumpMain.java   | 408 +++++++++++++++++++++
 .../sshd/common/config/keys/PublicKeyEntry.java    |   5 +
 .../org/apache/sshd/common/util/io/PathUtils.java  |   8 +-
 .../OpenSSHKeyPairResourceParserTestSupport.java   |   3 +-
 .../apache/sshd/common/util/io/PathUtilsTest.java  |   7 +-
 .../GenerateOpenSSHClientCertificateTest.java      |   6 +-
 ...GenerateOpenSshClientCertificateOracleTest.java |   4 +-
 .../certificates/OpenSSHCertificateParserTest.java |   2 +-
 .../ClientOpenSSHCertificatesTest.java             |  13 +-
 .../common/config/keys/AuthorizedKeyEntryTest.java |   2 +-
 .../FileHostKeyCertificateProviderTest.java        |   3 +-
 .../common/signature/OpenSSHCertificateTest.java   |   5 +-
 15 files changed, 501 insertions(+), 34 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 894d234..fb283e5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -69,7 +69,6 @@ Was originally in *HostConfigEntry*.
 * [SSHD-1233](https://issues.apache.org/jira/browse/SSHD-1233) Added support for "limits@openssh.com" SFTP extension
 * [SSHD-1244](https://issues.apache.org/jira/browse/SSHD-1244) Fixed channel window adjustment handling of large UINT32 values
 * [SSHD-1244](https://issues.apache.org/jira/browse/SSHD-1244) Re-defined channel identifiers as `long` rather than `int` to align with protocol UINT32 definition
-
-
+* [SSHD-1246](https://issues.apache.org/jira/browse/SSHD-1246) Added SshKeyDumpMain utility
 
 
diff --git a/docs/client-setup.md b/docs/client-setup.md
index 0fc059e..bd6df7e 100644
--- a/docs/client-setup.md
+++ b/docs/client-setup.md
@@ -50,6 +50,57 @@ and presenting them to the server as part of the authentication process. Reading
 for the standard keys and formats. Using additional non-standard special features requires that the [Bouncy Castle](https://www.bouncycastle.org/) supporting
 artifacts be available in the code's classpath.
 
+#### Loading key files
+
+In order to use password-less authentication the user needs to provide one or more `KeyPair`-s that are used to "prove" the client's identity for
+the server. The code supports most if not all of the currently used key file formats. See `SshKeyDumpMain` class for example of how to load files - basically:
+
+```java
+    KeyPairResourceLoader loader = SecurityUtils.getKeyPairResourceParser();
+    Collection<KeyPair> keys = loader.loadKeyPairs(null, filePath, passwordProvider);
+```
+
+For *PUTTY* key files one needs to include the *sshd-putty* module and use a different loader:
+
+```java
+    Collection<KeyPair> keys = PuttyKeyUtils.DEFAULT_INSTANCE.loadKeyPairs(null, filePath, passwordProvider);
+```
+
+**Note:** reminder - a user's "identity" is the file that contains the **private** key - there is no need to provide the public key file since the
+private key either already contains the public key in it, or it can be easily calculated from the private one.
+
+Once the keys are loaded, one simply needs to provide them to the client session:
+
+```java
+    try (ClientSession session = ...estblish initial session...) {
+        for (KeyPair kp : keys) {
+            session.addKeyIdentity(kp);
+        }
+        
+        session.auth().await(...);
+    }
+```
+
+Instead of doing this on every session, it is possible to load the keys only **once** and then wrap them inside a `KeyIdentityProvider`
+that is setup during *SshClient* setup:
+
+```java
+    Collection<KeyPair> keys = ...load the keys ...
+    SshClient client = ...setup client...
+    client.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keys));
+    client.start();
+```
+
+The provided keys will be used for **all* the sessions - *Note:*
+
+* One can **add** key identities to specific sessions.
+
+* A similar effect can be achiveved for **passwords**  by registering a `PasswordIdentityProvider` with the *SshClient*, and
+thus forego the need to provide the password repeatedly for each session. In this context, one can go even one step forward
+and provide a **combined** `AuthenticationIdentitiesProvider` that provides **both** passwords and key pairs. Both type of providers
+are invoked with the established `SessionContext` so the user can actually pick which mechanism to use, what password/key to
+use according to the server's identity.
+
 #### Providing passwords for encrypted key files
 
 The `FilePasswordProvider` is required for all private key files that are encrypted and being loaded (not just the "identity" ones). If the user
@@ -100,7 +151,7 @@ This interface is required for full support of `keyboard-interactive` authentica
 The client can handle a simple password request from the server, but if more complex challenge-response interaction is required, then this interface must be
 provided - including support for `SSH_MSG_USERAUTH_PASSWD_CHANGEREQ` as described in [RFC 4252 section 8](https://tools.ietf.org/html/rfc4252#section-8).
 
-While ]RFC-4256](https://tools.ietf.org/html/rfc4256) support is the primary purpose of this interface, it can also be used to retrieve the server's
+While [RFC-4256](https://tools.ietf.org/html/rfc4256) support is the primary purpose of this interface, it can also be used to retrieve the server's
 welcome banner as described in [RFC 4252 section 5.4](https://tools.ietf.org/html/rfc4252#section-5.4) as well as its initial identification string
 as described in [RFC 4253 section 4.2](https://tools.ietf.org/html/rfc4253#section-4.2).
 
diff --git a/sshd-cli/pom.xml b/sshd-cli/pom.xml
index d2d099b..cb5d62b 100644
--- a/sshd-cli/pom.xml
+++ b/sshd-cli/pom.xml
@@ -55,6 +55,12 @@
             <artifactId>sshd-putty</artifactId>
             <version>${project.version}</version>
         </dependency>
+            <!-- For ed25519 support -->
+        <dependency>
+            <groupId>net.i2p.crypto</groupId>
+            <artifactId>eddsa</artifactId>
+            <optional>true</optional>
+        </dependency>
 
             <!-- Test dependencies -->
         <dependency>
@@ -95,13 +101,6 @@
             <artifactId>jzlib</artifactId>
             <scope>test</scope>
         </dependency>
-            <!-- For ed25519 support -->
-        <dependency>
-            <groupId>net.i2p.crypto</groupId>
-            <artifactId>eddsa</artifactId>
-            <optional>true</optional>
-            <scope>test</scope>
-        </dependency>
     </dependencies>
 
     <build>
diff --git a/sshd-cli/src/test/java/org/apache/sshd/cli/SshKeyDumpMain.java b/sshd-cli/src/test/java/org/apache/sshd/cli/SshKeyDumpMain.java
new file mode 100644
index 0000000..330d687
--- /dev/null
+++ b/sshd-cli/src/test/java/org/apache/sshd/cli/SshKeyDumpMain.java
@@ -0,0 +1,408 @@
+/*
+ * 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.cli;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.Key;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.DSAParams;
+import java.security.interfaces.DSAPrivateKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPrivateCrtKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.ECField;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.EllipticCurve;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import net.i2p.crypto.eddsa.EdDSAPrivateKey;
+import net.i2p.crypto.eddsa.EdDSAPublicKey;
+import net.i2p.crypto.eddsa.math.Curve;
+import net.i2p.crypto.eddsa.math.Field;
+import net.i2p.crypto.eddsa.math.GroupElement;
+import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.config.keys.loader.KeyPairResourceLoader;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.io.PathUtils;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.apache.sshd.putty.PuttyKeyPairResourceParser;
+import org.apache.sshd.putty.PuttyKeyUtils;
+import org.apache.sshd.server.config.keys.AuthorizedKeysAuthenticator;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public enum SshKeyDumpMain {
+    /* Utility class */;
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public static void dumpRSAPublicKey(RSAPublicKey key, CharSequence indent, Appendable stdout) throws IOException {
+        stdout.append(indent)
+                .append("e: ").append(Objects.toString(key.getPublicExponent(), null))
+                .append(System.lineSeparator());
+        stdout.append(indent)
+                .append("n: ").append(Objects.toString(key.getModulus(), null))
+                .append(System.lineSeparator());
+    }
+
+    public static void dumpRSAPrivateKey(RSAPrivateKey key, CharSequence indent, Appendable stdout) throws IOException {
+        stdout.append(indent)
+                .append("d: ").append(Objects.toString(key.getPrivateExponent(), null))
+                .append(System.lineSeparator());
+        stdout.append(indent)
+                .append("n: ").append(Objects.toString(key.getModulus(), null))
+                .append(System.lineSeparator());
+        if (key instanceof RSAPrivateCrtKey) {
+            RSAPrivateCrtKey crt = RSAPrivateCrtKey.class.cast(key);
+            stdout.append(indent)
+                    .append("e: ").append(Objects.toString(crt.getPublicExponent(), null))
+                    .append(System.lineSeparator());
+            stdout.append(indent)
+                    .append("P: ").append(Objects.toString(crt.getPrimeP(), null))
+                    .append(System.lineSeparator());
+            stdout.append(indent)
+                    .append("Q: ").append(Objects.toString(crt.getPrimeQ(), null))
+                    .append(System.lineSeparator());
+            stdout.append(indent)
+                    .append("expP: ").append(Objects.toString(crt.getPrimeExponentP(), null))
+                    .append(System.lineSeparator());
+            stdout.append(indent)
+                    .append("expQ: ").append(Objects.toString(crt.getPrimeExponentQ(), null))
+                    .append(System.lineSeparator());
+            stdout.append(indent)
+                    .append("coefficient: ").append(Objects.toString(crt.getCrtCoefficient(), null))
+                    .append(System.lineSeparator());
+        }
+    }
+
+    public static void dumpDSAParams(DSAParams params, CharSequence indent, Appendable stdout) throws IOException {
+        stdout.append(indent)
+                .append("G: ").append(Objects.toString(params.getG(), null))
+                .append(System.lineSeparator());
+        stdout.append(indent)
+                .append("P: ").append(Objects.toString(params.getP(), null))
+                .append(System.lineSeparator());
+        stdout.append(indent)
+                .append("Q: ").append(Objects.toString(params.getQ(), null))
+                .append(System.lineSeparator());
+    }
+
+    public static void dumpDSAPublicKey(DSAPublicKey key, CharSequence indent, Appendable stdout) throws IOException {
+        stdout.append(indent)
+                .append("Y: ").append(Objects.toString(key.getY(), null))
+                .append(System.lineSeparator());
+        dumpDSAParams(key.getParams(), indent + "    ",
+                stdout.append(indent).append("params:").append(System.lineSeparator()));
+    }
+
+    public static void dumpDSAPrivateKey(DSAPrivateKey key, CharSequence indent, Appendable stdout) throws IOException {
+        stdout.append(indent)
+                .append("X: ").append(Objects.toString(key.getX(), null))
+                .append(System.lineSeparator());
+        dumpDSAParams(key.getParams(), indent + "    ",
+                stdout.append(indent).append("params:").append(System.lineSeparator()));
+    }
+
+    public static void dumpECPoint(ECPoint point, CharSequence indent, Appendable stdout) throws IOException {
+        stdout.append(indent)
+                .append("X: ").append(Objects.toString(point.getAffineX(), null))
+                .append(System.lineSeparator());
+        stdout.append(indent)
+                .append("Y: ").append(Objects.toString(point.getAffineY(), null))
+                .append(System.lineSeparator());
+    }
+
+    public static void dumpECField(ECField field, CharSequence indent, Appendable stdout) throws IOException {
+        stdout.append(indent)
+                .append("size: ").append(Integer.toString(field.getFieldSize()))
+                .append(System.lineSeparator());
+    }
+
+    public static void dumpEllipticCurve(EllipticCurve curve, CharSequence indent, Appendable stdout) throws IOException {
+        stdout.append(indent)
+                .append("A: ").append(Objects.toString(curve.getA(), null))
+                .append(System.lineSeparator());
+        stdout.append(indent)
+                .append("B: ").append(Objects.toString(curve.getB(), null))
+                .append(System.lineSeparator());
+        BufferUtils.appendHex(stdout.append(indent).append("seed: "), ' ', curve.getSeed()).append(System.lineSeparator());
+        dumpECField(curve.getField(), indent + "    ",
+                stdout.append(indent).append("field:").append(System.lineSeparator()));
+    }
+
+    public static void dumpECParameterSpec(ECParameterSpec spec, CharSequence indent, Appendable stdout) throws IOException {
+        stdout.append(indent)
+                .append("order: ").append(Objects.toString(spec.getOrder(), null))
+                .append(System.lineSeparator());
+        stdout.append(indent)
+                .append("cofactor: ").append(Integer.toString(spec.getCofactor()))
+                .append(System.lineSeparator());
+        dumpEllipticCurve(spec.getCurve(), indent + "    ",
+                stdout.append(indent).append("curve:").append(System.lineSeparator()));
+        dumpECPoint(spec.getGenerator(), indent + "    ",
+                stdout.append(indent).append("generator:").append(System.lineSeparator()));
+    }
+
+    public static void dumpECPublicKey(ECPublicKey key, CharSequence indent, Appendable stdout) throws IOException {
+        stdout.append(indent)
+                .append("W: ").append(Objects.toString(key.getW(), null))
+                .append(System.lineSeparator());
+        dumpECParameterSpec(key.getParams(), indent + "    ",
+                stdout.append(indent).append("params:").append(System.lineSeparator()));
+    }
+
+    public static void dumpECPrivateKey(ECPrivateKey key, CharSequence indent, Appendable stdout) throws IOException {
+        stdout.append(indent)
+                .append("S: ").append(Objects.toString(key.getS(), null))
+                .append(System.lineSeparator());
+        dumpECParameterSpec(key.getParams(), indent + "    ",
+                stdout.append(indent).append("params:").append(System.lineSeparator()));
+    }
+
+    public static void dumpEdDSAField(Field field, CharSequence indent, Appendable stdout) throws IOException {
+        stdout.append(indent)
+                .append("Q: ").append(Objects.toString(field.getQ(), null))
+                .append(System.lineSeparator());
+    }
+
+    public static void dumpEdDSACurve(Curve curve, CharSequence indent, Appendable stdout) throws IOException {
+        dumpEdDSAField(curve.getField(), indent + "    ",
+                stdout.append(indent).append("field: ").append(System.lineSeparator()));
+        stdout.append(indent)
+                .append("D: ").append(Objects.toString(curve.getD(), null))
+                .append(System.lineSeparator());
+        stdout.append(indent)
+                .append("I: ").append(Objects.toString(curve.getI(), null))
+                .append(System.lineSeparator());
+    }
+
+    public static void dumpEdDSAGroupElement(GroupElement group, CharSequence indent, Appendable stdout) throws IOException {
+        dumpEdDSACurve(group.getCurve(), indent + "    ",
+                stdout.append(indent).append("curve:").append(System.lineSeparator()));
+        stdout.append(indent)
+                .append("X: ").append(Objects.toString(group.getX(), null))
+                .append(System.lineSeparator());
+        stdout.append(indent)
+                .append("Y: ").append(Objects.toString(group.getY(), null))
+                .append(System.lineSeparator());
+        stdout.append(indent)
+                .append("Z: ").append(Objects.toString(group.getZ(), null))
+                .append(System.lineSeparator());
+        stdout.append(indent)
+                .append("T: ").append(Objects.toString(group.getT(), null))
+                .append(System.lineSeparator());
+    }
+
+    public static void dumpEdDSAParameterSpec(EdDSAParameterSpec params, CharSequence indent, Appendable stdout)
+            throws IOException {
+        dumpEdDSAGroupElement(params.getB(), indent + "    ",
+                stdout.append(indent).append("B:").append(System.lineSeparator()));
+        stdout.append(indent)
+                .append("hashAlgorith,: ").append(params.getHashAlgorithm())
+                .append(System.lineSeparator());
+        dumpEdDSACurve(params.getCurve(), indent + "    ",
+                stdout.append(indent).append("curve:").append(System.lineSeparator()));
+    }
+
+    public static void dumpEdDSAPublicKey(EdDSAPublicKey key, CharSequence indent, Appendable stdout) throws IOException {
+        dumpEdDSAGroupElement(key.getA(), indent + "    ",
+                stdout.append(indent).append("A:").append(System.lineSeparator()));
+        dumpEdDSAParameterSpec(key.getParams(), indent + "    ",
+                stdout.append(indent).append("params:").append(System.lineSeparator()));
+    }
+
+    public static void dumpEdDSAPrivateKey(EdDSAPrivateKey key, CharSequence indent, Appendable stdout) throws IOException {
+        dumpEdDSAGroupElement(key.getA(), indent + "    ",
+                stdout.append(indent).append("A:").append(System.lineSeparator()));
+        BufferUtils.appendHex(stdout.append(indent).append("seed: "), ' ', key.getSeed()).append(System.lineSeparator());
+        BufferUtils.appendHex(stdout.append(indent).append("H: "), ' ', key.getH()).append(System.lineSeparator());
+        dumpEdDSAParameterSpec(key.getParams(), indent + "    ",
+                stdout.append(indent).append("params:").append(System.lineSeparator()));
+    }
+
+    public static void dumpPublicKey(PublicKey key, CharSequence indent, Appendable stdout, Appendable stderr)
+            throws IOException {
+        if (key instanceof RSAPublicKey) {
+            dumpRSAPublicKey(
+                    RSAPublicKey.class.cast(key), indent + "    ",
+                    stdout.append(indent).append("RSA").append(System.lineSeparator()));
+            return;
+        } else if (key instanceof DSAPublicKey) {
+            dumpDSAPublicKey(
+                    DSAPublicKey.class.cast(key), indent + "    ",
+                    stdout.append(indent).append("DSA").append(System.lineSeparator()));
+            return;
+        } else if (key instanceof ECPublicKey) {
+            dumpECPublicKey(
+                    ECPublicKey.class.cast(key), indent + "    ",
+                    stdout.append(indent).append("EC").append(System.lineSeparator()));
+            return;
+        } else if (SecurityUtils.isEDDSACurveSupported()) {
+            if (key instanceof EdDSAPublicKey) {
+                dumpEdDSAPublicKey(
+                        EdDSAPublicKey.class.cast(key), indent + "    ",
+                        stdout.append(indent).append("EdDSA").append(System.lineSeparator()));
+                return;
+            }
+        }
+
+        if (stderr != null) {
+            stderr.append(indent)
+                    .append("Unsupported public key type: ")
+                    .append(key.getClass().getName())
+                    .append(System.lineSeparator());
+        } else {
+            throw new UnsupportedOperationException("Unsupported public key type: " + key.getClass().getName());
+        }
+    }
+
+    public static void dumpPrivateKey(PrivateKey key, CharSequence indent, Appendable stdout, Appendable stderr)
+            throws IOException {
+        if (key instanceof RSAPrivateKey) {
+            dumpRSAPrivateKey(RSAPrivateKey.class.cast(key), indent + "    ",
+                    stdout.append(indent).append("RSA").append(System.lineSeparator()));
+            return;
+        } else if (key instanceof DSAPrivateKey) {
+            dumpDSAPrivateKey(DSAPrivateKey.class.cast(key), indent + "    ",
+                    stdout.append(indent).append("DSA").append(System.lineSeparator()));
+            return;
+        } else if (key instanceof ECPrivateKey) {
+            dumpECPrivateKey(ECPrivateKey.class.cast(key), indent + "    ",
+                    stdout.append(indent).append("EC").append(System.lineSeparator()));
+            return;
+        } else if (SecurityUtils.isEDDSACurveSupported()) {
+            if (key instanceof EdDSAPrivateKey) {
+                dumpEdDSAPrivateKey(EdDSAPrivateKey.class.cast(key), indent + "    ",
+                        stdout.append(indent).append("EC").append(System.lineSeparator()));
+                return;
+            }
+        }
+
+        if (stderr != null) {
+            stderr.append(indent)
+                    .append("Unsupported private key type: ")
+                    .append(key.getClass().getName())
+                    .append(System.lineSeparator());
+        } else {
+            throw new UnsupportedOperationException("Unsupported private key type: " + key.getClass().getName());
+        }
+    }
+
+    public static void dumpKey(Key key, CharSequence indent, Appendable stdout, Appendable stderr) throws IOException {
+        if (key instanceof PublicKey) {
+            dumpPublicKey(PublicKey.class.cast(key), indent, stdout, stderr);
+        } else if (key instanceof PrivateKey) {
+            dumpPrivateKey(PrivateKey.class.cast(key), indent, stdout, stderr);
+        } else if (stderr != null) {
+            stderr.append(indent)
+                    .append("Unknown key type: ").append(key.getClass().getSimpleName())
+                    .append(System.lineSeparator());
+        } else {
+            throw new ClassCastException("Unknown key type: " + key.getClass().getSimpleName());
+        }
+    }
+
+    public static void dumpKeyFileData(Path filePath, String password, Appendable stdout, Appendable stderr) throws Exception {
+        FilePasswordProvider passwordProvider = GenericUtils.isEmpty(password)
+                ? FilePasswordProvider.EMPTY
+                : FilePasswordProvider.of(password);
+        String fileName = filePath.getFileName().toString();
+        Collection<KeyPair> keys;
+        if (fileName.endsWith(PuttyKeyPairResourceParser.PPK_FILE_SUFFIX)) {
+            keys = PuttyKeyUtils.DEFAULT_INSTANCE.loadKeyPairs(null, filePath, passwordProvider);
+        } else if (fileName.endsWith(PublicKeyEntry.PUBKEY_FILE_SUFFIX)
+                || AuthorizedKeysAuthenticator.STD_AUTHORIZED_KEYS_FILENAME.equals(fileName)) {
+            List<? extends PublicKeyEntry> entries = AuthorizedKeyEntry.readAuthorizedKeys(filePath);
+            int numEntries = GenericUtils.size(entries);
+            keys = (numEntries <= 0)
+                    ? Collections.emptyList()
+                    : new ArrayList<>(entries.size());
+            for (int index = 0; index < numEntries; index++) {
+                PublicKeyEntry e = entries.get(index);
+                PublicKey pubKey = e.resolvePublicKey(null, Collections.emptyMap(), null);
+                if (pubKey == null) {
+                    if (stderr != null) {
+                        stderr.append("Cannot resolve public entry=").append(e.toString()).append(System.lineSeparator());
+                    } else {
+                        throw new UnsupportedOperationException("Cannot resolve public entry=" + e);
+                    }
+                    continue;
+                }
+
+                keys.add(new KeyPair(pubKey, null));
+            }
+        } else {
+            KeyPairResourceLoader loader = SecurityUtils.getKeyPairResourceParser();
+            keys = loader.loadKeyPairs(null, filePath, passwordProvider);
+        }
+
+        if (GenericUtils.isEmpty(keys)) {
+            if (stderr != null) {
+                stderr.append("No keys found in ").append(filePath.toString()).append(System.lineSeparator());
+                return;
+            } else {
+                throw new IllegalArgumentException("No keys found in " + filePath);
+            }
+        }
+
+        for (KeyPair kp : keys) {
+            PublicKey pubKey = kp.getPublic();
+            PublicKeyEntry.appendPublicKeyEntry(stdout.append("Public key: "), pubKey).append(System.lineSeparator());
+            dumpPublicKey(pubKey, "    ", stdout, stderr);
+
+            PrivateKey prvKey = kp.getPrivate();
+            if (prvKey != null) {
+                stdout.append("Private key:").append(System.lineSeparator());
+                dumpPrivateKey(kp.getPrivate(), "    ", stdout, stderr);
+            }
+        }
+    }
+
+    /////////////////////////////////////////////////////////////////////////////////////////
+
+    public static void main(String[] args) throws Exception {
+        int numArgs = GenericUtils.length(args);
+        if (numArgs <= 0) {
+            System.err.println("Usage: path [password]");
+            return;
+        }
+
+        String filePath = PathUtils.normalizePath(args[0]);
+        String password = (numArgs > 1) ? args[1] : null;
+        dumpKeyFileData(Paths.get(filePath), password, System.out, System.err);
+    }
+}
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java
index 882daa7..8e7b77e 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java
@@ -67,6 +67,11 @@ public class PublicKeyEntry implements Serializable, KeyTypeIndicator {
      */
     public static final String STD_KEYFILE_FOLDER_NAME = ".ssh";
 
+    /**
+     * Standard suffix for SSH public key files
+     */
+    public static final String PUBKEY_FILE_SUFFIX = ".pub";
+
     private static final long serialVersionUID = -585506072687602760L;
 
     private static final NavigableMap<String, PublicKeyEntryDataResolver> KEY_DATA_RESOLVERS
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java
index cd9ab9c..4598cd6 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/io/PathUtils.java
@@ -92,12 +92,12 @@ public final class PathUtils {
 
     /**
      * <UL>
-     *      <LI>Replaces <U>leading</U> '~' with user's HOME directory</LI>
-     *      <LI>Replaces any forward slashes with the O/S directory separator</LI>
+     * <LI>Replaces <U>leading</U> '~' with user's HOME directory</LI>
+     * <LI>Replaces any forward slashes with the O/S directory separator</LI>
      * </UL>
      *
-     * @param path Input path - ignored if {@code null}/empty/blank
-     * @return Adjusted path
+     * @param  path Input path - ignored if {@code null}/empty/blank
+     * @return      Adjusted path
      */
     public static String normalizePath(String path) {
         if (GenericUtils.isBlank(path)) {
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserTestSupport.java b/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserTestSupport.java
index 7d0cd7e..24a071a 100644
--- a/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserTestSupport.java
+++ b/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserTestSupport.java
@@ -30,6 +30,7 @@ import org.apache.sshd.common.cipher.BuiltinCiphers;
 import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
 import org.apache.sshd.common.config.keys.BuiltinIdentities;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.util.test.JUnitTestSupport;
@@ -80,7 +81,7 @@ public abstract class OpenSSHKeyPairResourceParserTestSupport extends JUnitTestS
             throw e;
         }
 
-        URL urlPubKey = getClass().getResource(resourceKey + ".pub");
+        URL urlPubKey = getClass().getResource(resourceKey + PublicKeyEntry.PUBKEY_FILE_SUFFIX);
         assertNotNull("Missing public key resource: " + resourceKey, urlPubKey);
 
         List<AuthorizedKeyEntry> entries = AuthorizedKeyEntry.readAuthorizedKeys(urlPubKey);
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/util/io/PathUtilsTest.java b/sshd-common/src/test/java/org/apache/sshd/common/util/io/PathUtilsTest.java
index 32c0480..873190f 100644
--- a/sshd-common/src/test/java/org/apache/sshd/common/util/io/PathUtilsTest.java
+++ b/sshd-common/src/test/java/org/apache/sshd/common/util/io/PathUtilsTest.java
@@ -50,11 +50,10 @@ public class PathUtilsTest extends JUnitTestSupport {
     public void testNormalizeLeadingUserHomePath() {
         Path expected = PathUtils.getUserHomeFolder()
                 .resolve(getClass().getSimpleName())
-                .resolve(getCurrentTestName())
-                ;
+                .resolve(getCurrentTestName());
         String actual = PathUtils.normalizePath(PathUtils.HOME_TILDE_CHAR
-            + File.separator + getClass().getSimpleName()
-            + File.separator + getCurrentTestName());
+                                                + File.separator + getClass().getSimpleName()
+                                                + File.separator + getCurrentTestName());
         assertEquals(expected.toString(), actual);
     }
 
diff --git a/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSSHClientCertificateTest.java b/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSSHClientCertificateTest.java
index 32462e8..4ca9e2c 100644
--- a/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSSHClientCertificateTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSSHClientCertificateTest.java
@@ -93,7 +93,7 @@ public class GenerateOpenSSHClientCertificateTest extends BaseTestSupport {
     }
 
     protected String getCAPublicKeyResource() {
-        return getCAPrivateKeyResource() + ".pub";
+        return getCAPrivateKeyResource() + PublicKeyEntry.PUBKEY_FILE_SUFFIX;
     }
 
     protected String getClientPrivateKeyResource() {
@@ -101,11 +101,11 @@ public class GenerateOpenSSHClientCertificateTest extends BaseTestSupport {
     }
 
     protected String getClientPublicKeyResource() {
-        return getClientPrivateKeyResource() + ".pub";
+        return getClientPrivateKeyResource() + PublicKeyEntry.PUBKEY_FILE_SUFFIX;
     }
 
     protected String getOracle() {
-        return getClientPrivateKeyResource() + "-cert.pub";
+        return getClientPrivateKeyResource() + "-cert" + PublicKeyEntry.PUBKEY_FILE_SUFFIX;
     }
 
     protected PublicKey readPublicKeyFromResource(String resource) throws Exception {
diff --git a/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSshClientCertificateOracleTest.java b/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSshClientCertificateOracleTest.java
index 3cb7ff8..a37d1bf 100644
--- a/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSshClientCertificateOracleTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/certificates/GenerateOpenSshClientCertificateOracleTest.java
@@ -72,11 +72,11 @@ public class GenerateOpenSshClientCertificateOracleTest extends BaseTestSupport
     }
 
     protected String getClientPublicKeyResource() {
-        return getClientPrivateKeyResource() + ".pub";
+        return getClientPrivateKeyResource() + PublicKeyEntry.PUBKEY_FILE_SUFFIX;
     }
 
     protected String getOracle() {
-        return getClientPrivateKeyResource() + "-cert.pub";
+        return getClientPrivateKeyResource() + "-cert" + PublicKeyEntry.PUBKEY_FILE_SUFFIX;
     }
 
     protected PublicKey readPublicKeyFromResource(String resource) throws Exception {
diff --git a/sshd-core/src/test/java/org/apache/sshd/certificates/OpenSSHCertificateParserTest.java b/sshd-core/src/test/java/org/apache/sshd/certificates/OpenSSHCertificateParserTest.java
index adcb0be..734f5ae 100644
--- a/sshd-core/src/test/java/org/apache/sshd/certificates/OpenSSHCertificateParserTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/certificates/OpenSSHCertificateParserTest.java
@@ -64,7 +64,7 @@ public class OpenSSHCertificateParserTest extends BaseTestSupport {
 
     @SuppressWarnings("synthetic-access")
     private String getCertificateResource() {
-        return USER_KEY_PATH + params.privateKey + "-cert.pub";
+        return USER_KEY_PATH + params.privateKey + "-cert" + PublicKeyEntry.PUBKEY_FILE_SUFFIX;
     }
 
     @Test
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/opensshcerts/ClientOpenSSHCertificatesTest.java b/sshd-core/src/test/java/org/apache/sshd/client/opensshcerts/ClientOpenSSHCertificatesTest.java
index 535f39f..5392a2e 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/opensshcerts/ClientOpenSSHCertificatesTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/opensshcerts/ClientOpenSSHCertificatesTest.java
@@ -88,10 +88,13 @@ public class ClientOpenSSHCertificatesTest extends BaseTestSupport {
                     .withFileFromClasspath("user02_authorized_keys",
                             "org/apache/sshd/client/opensshcerts/user/user02_authorized_keys")
                     .withFileFromClasspath("host01", "org/apache/sshd/client/opensshcerts/host/host01")
-                    .withFileFromClasspath("host01.pub", "org/apache/sshd/client/opensshcerts/host/host01.pub")
+                    .withFileFromClasspath("host01" + PublicKeyEntry.PUBKEY_FILE_SUFFIX,
+                            "org/apache/sshd/client/opensshcerts/host/host01" + PublicKeyEntry.PUBKEY_FILE_SUFFIX)
                     .withFileFromClasspath("host02", "org/apache/sshd/client/opensshcerts/host/host02")
-                    .withFileFromClasspath("host02.pub", "org/apache/sshd/client/opensshcerts/host/host02.pub")
-                    .withFileFromClasspath("ca.pub", "org/apache/sshd/client/opensshcerts/ca/ca.pub")
+                    .withFileFromClasspath("host02" + PublicKeyEntry.PUBKEY_FILE_SUFFIX,
+                            "org/apache/sshd/client/opensshcerts/host/host02" + PublicKeyEntry.PUBKEY_FILE_SUFFIX)
+                    .withFileFromClasspath("ca" + PublicKeyEntry.PUBKEY_FILE_SUFFIX,
+                            "org/apache/sshd/client/opensshcerts/ca/ca" + PublicKeyEntry.PUBKEY_FILE_SUFFIX)
                     .withFileFromClasspath("Dockerfile", "org/apache/sshd/client/opensshcerts/docker/Dockerfile"))
                             // must be set to "/keys/host/host01" or "/keys/host/host02"
                             .withEnv("SSH_HOST_KEY", "/keys/host/host01")
@@ -111,7 +114,7 @@ public class ClientOpenSSHCertificatesTest extends BaseTestSupport {
         Security.addProvider(new BouncyCastleProvider());
     }
 
-    @Parameterized.Parameters(name = "key: {0}, cert: {0}-cert.pub")
+    @Parameterized.Parameters(name = "key: {0}, cert: {0}-cert" + PublicKeyEntry.PUBKEY_FILE_SUFFIX)
     public static Iterable<? extends String> privateKeyParams() {
         return Arrays.asList(
                 "user01_rsa_sha2_256_2048",
@@ -129,7 +132,7 @@ public class ClientOpenSSHCertificatesTest extends BaseTestSupport {
     }
 
     private String getCertificateResource() {
-        return getPrivateKeyResource() + "-cert.pub";
+        return getPrivateKeyResource() + "-cert" + PublicKeyEntry.PUBKEY_FILE_SUFFIX;
     }
 
     @Test
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryTest.java b/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryTest.java
index aa83e15..3e4b53c 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryTest.java
@@ -103,7 +103,7 @@ public class AuthorizedKeyEntryTest extends AuthorizedKeysTestSupport {
     @Test
     @Ignore("Used to test specific files")
     public void testSpecificFile() throws Exception {
-        Path path = Paths.get("C:" + File.separator + "Temp", "id_ed25519.pub");
+        Path path = Paths.get("C:" + File.separator + "Temp", "id_ed25519" + PublicKeyEntry.PUBKEY_FILE_SUFFIX);
         testReadAuthorizedKeys(AuthorizedKeyEntry.readAuthorizedKeys(path));
     }
 
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/keyprovider/FileHostKeyCertificateProviderTest.java b/sshd-core/src/test/java/org/apache/sshd/common/keyprovider/FileHostKeyCertificateProviderTest.java
index 8d0c967..06ef81b 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/keyprovider/FileHostKeyCertificateProviderTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/keyprovider/FileHostKeyCertificateProviderTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sshd.common.keyprovider;
 
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.util.test.JUnitTestSupport;
 import org.junit.Test;
 
@@ -30,7 +31,7 @@ public class FileHostKeyCertificateProviderTest extends JUnitTestSupport {
     @Test
     public void testLoadingUserCertificateFails() {
         FileHostKeyCertificateProvider provider = new FileHostKeyCertificateProvider(
-                getTestResourcesFolder().resolve("dummy_user-cert.pub"));
+                getTestResourcesFolder().resolve("dummy_user-cert" + PublicKeyEntry.PUBKEY_FILE_SUFFIX));
         Exception e = assertThrows(Exception.class, () -> provider.loadCertificates(null));
         assertTrue("Expected error in line 1", e.getMessage().contains("line 1"));
         assertTrue("Unexpected exception message: " + e.getMessage(),
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSSHCertificateTest.java b/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSSHCertificateTest.java
index b4d7d6e..d266af8 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSSHCertificateTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/signature/OpenSSHCertificateTest.java
@@ -30,6 +30,7 @@ import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.keyprovider.FileHostKeyCertificateProvider;
 import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
 import org.apache.sshd.common.util.GenericUtils;
@@ -103,8 +104,8 @@ public class OpenSSHCertificateTest extends BaseTestSupport {
         List<Object[]> list = new ArrayList<>();
 
         String key = "ssh_host_rsa_key";
-        String certificate = "ssh_host_rsa_key_sha1-cert.pub";
-        String certificateSha512 = "ssh_host_rsa_key-cert.pub";
+        String certificate = "ssh_host_rsa_key_sha1-cert" + PublicKeyEntry.PUBKEY_FILE_SUFFIX;
+        String certificateSha512 = "ssh_host_rsa_key-cert" + PublicKeyEntry.PUBKEY_FILE_SUFFIX;
 
         // default client
         list.add(new Object[] {