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:52 UTC

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

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[] {