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 2020/05/22 13:00:40 UTC

[mina-sshd] 04/05: [SSHD-989] Added verification that parsed/recovered key pairs can be used for signing

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 1b2e34a11636ad62ae9092dc30e2c320d5e23dcf
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri May 22 08:59:15 2020 +0300

    [SSHD-989] Added verification that parsed/recovered key pairs can be used for signing
---
 CHANGES.md                                         |  4 +-
 .../loader/pem/ECDSAPEMResourceKeyPairParser.java  |  5 ++
 .../sshd/common/signature/AbstractSignature.java   | 23 ++++--
 .../apache/sshd/common/signature/SignatureDSA.java |  2 +-
 .../sshd/common/signature/SignatureECDSA.java      |  2 +-
 .../apache/sshd/common/signature/SignatureRSA.java | 22 ++++--
 .../security/eddsa/EdDSASecurityProviderUtils.java |  7 +-
 .../util/security/eddsa/SignatureEd25519.java      |  3 +-
 .../OpenSSHKeyPairResourceParserDecodingTest.java  |  2 +
 .../OpenSSHKeyPairResourceParserPasswordTest.java  |  1 +
 .../pem/PKCS8PEMResourceKeyPairParserTest.java     | 36 +++++++++
 .../common/signature/SignatureRSASHA1Test.java     |  3 +-
 .../common/util/security/SecurityUtilsTest.java    | 10 +--
 .../sshd/util/test/CommonTestSupportUtils.java     | 86 +++++++++++++++++++++-
 .../apache/sshd/util/test/JUnitTestSupport.java    |  9 +++
 15 files changed, 191 insertions(+), 24 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 8e644be..1495950 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -53,10 +53,8 @@ as much of the available functionality as possible.
 
 * [SSHD-992](https://issues.apache.org/jira/browse/SSHD-984) - Provide more hooks into the SFTP server subsystem via SftpFileSystemAccessor
 
-<<<<<<< HEAD
 * [SSHD-997](https://issues.apache.org/jira/browse/SSHD-997) - Fixed OpenSSH private key decoders for RSA and Ed25519
 
 * [SSHD-998](https://issues.apache.org/jira/browse/SSHD-998) - Take into account SFTP version preference when establishing initial channel
-=======
+
 * [SSHD-989](https://issues.apache.org/jira/browse/SSHD-989) - Implement ECDSA public key recovery for PKCS8 encoded data
->>>>>>> [SSHD-989] Implement ECDSA public key recovery from PKCS#8 encoded data
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/ECDSAPEMResourceKeyPairParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/ECDSAPEMResourceKeyPairParser.java
index 44d3d51..13b3f91 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/ECDSAPEMResourceKeyPairParser.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/ECDSAPEMResourceKeyPairParser.java
@@ -224,6 +224,11 @@ public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar
                 throw new StreamCorruptedException("Missing named curve parameter");
             }
 
+            /*
+             * SSHD-989 - if object type is BIT STRING then this is the public key - in which case we need to figure out
+             * some other way to recover the curve parameters
+             */
+
             curveOID = namedCurve.asOID();
         }
 
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/signature/AbstractSignature.java b/sshd-common/src/main/java/org/apache/sshd/common/signature/AbstractSignature.java
index 8381d32..d167788 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/signature/AbstractSignature.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/signature/AbstractSignature.java
@@ -25,9 +25,13 @@ import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.SignatureException;
 import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.Collection;
+import java.util.Map;
 import java.util.Objects;
+import java.util.function.Predicate;
 
 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.buffer.BufferUtils;
@@ -109,12 +113,17 @@ public abstract class AbstractSignature implements Signature {
     /**
      * Makes an attempt to detect if the signature is encoded or pure data
      *
-     * @param  sig The original signature
-     * @return     A {@link SimpleImmutableEntry} where first value is the key type and second value is the data -
-     *             {@code null} if not encoded
+     * @param  sig           The original signature
+     * @param  expectedTypes The expected encoded key types
+     * @return               A {@link SimpleImmutableEntry} where first value is the key type and second value is the
+     *                       data - {@code null} if not encoded
      */
-    protected SimpleImmutableEntry<String, byte[]> extractEncodedSignature(byte[] sig) {
-        final int dataLen = NumberUtils.length(sig);
+    protected Map.Entry<String, byte[]> extractEncodedSignature(byte[] sig, Collection<String> expectedTypes) {
+        return GenericUtils.isEmpty(expectedTypes) ? null : extractEncodedSignature(sig, k -> expectedTypes.contains(k));
+    }
+
+    protected Map.Entry<String, byte[]> extractEncodedSignature(byte[] sig, Predicate<? super String> typeSelector) {
+        int dataLen = NumberUtils.length(sig);
         // if it is encoded then we must have at least 2 UINT32 values
         if (dataLen < (2 * Integer.BYTES)) {
             return null;
@@ -141,6 +150,10 @@ public abstract class AbstractSignature implements Signature {
         }
 
         String keyType = new String(sig, keyTypeStartPos, (int) keyTypeLen, StandardCharsets.UTF_8);
+        if (!typeSelector.test(keyType)) {
+            return null;
+        }
+
         byte[] data = new byte[(int) dataBytesLen];
         System.arraycopy(sig, keyTypeEndPos + Integer.BYTES, data, 0, (int) dataBytesLen);
         return new SimpleImmutableEntry<>(keyType, data);
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureDSA.java b/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureDSA.java
index d44d222..4ee5785 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureDSA.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureDSA.java
@@ -102,7 +102,7 @@ public class SignatureDSA extends AbstractSignature {
 
         if (sigLen != DSA_SIGNATURE_LENGTH) {
             // probably some encoded data
-            Map.Entry<String, byte[]> encoding = extractEncodedSignature(sig);
+            Map.Entry<String, byte[]> encoding = extractEncodedSignature(sig, k -> KeyPairProvider.SSH_DSS.equalsIgnoreCase(k));
             if (encoding != null) {
                 String keyType = encoding.getKey();
                 ValidateUtils.checkTrue(
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureECDSA.java b/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureECDSA.java
index e8e705b..33e4251 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureECDSA.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureECDSA.java
@@ -103,7 +103,7 @@ public class SignatureECDSA extends AbstractSignature {
     @Override
     public boolean verify(SessionContext session, byte[] sig) throws Exception {
         byte[] data = sig;
-        Map.Entry<String, byte[]> encoding = extractEncodedSignature(data);
+        Map.Entry<String, byte[]> encoding = extractEncodedSignature(data, ECCurves.KEY_TYPES);
         if (encoding != null) {
             String keyType = encoding.getKey();
             ECCurves curve = ECCurves.fromKeyType(keyType);
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureRSA.java b/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureRSA.java
index a41a89b..0639a45 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureRSA.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureRSA.java
@@ -21,7 +21,12 @@ package org.apache.sshd.common.signature;
 import java.math.BigInteger;
 import java.security.PublicKey;
 import java.security.interfaces.RSAKey;
+import java.util.Collections;
 import java.util.Map;
+import java.util.NavigableSet;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
@@ -35,6 +40,16 @@ import org.apache.sshd.common.util.ValidateUtils;
  * @see    <A HREF="https://tools.ietf.org/html/rfc4253#section-6.6">RFC4253 section 6.6</A>
  */
 public abstract class SignatureRSA extends AbstractSignature {
+    public static final NavigableSet<String> SUPPORTED_KEY_TYPES = Collections.unmodifiableNavigableSet(
+            Stream.of(
+                    KeyPairProvider.SSH_RSA,
+                    KeyPairProvider.SSH_RSA_CERT,
+                    KeyUtils.RSA_SHA256_KEY_TYPE_ALIAS,
+                    KeyUtils.RSA_SHA512_KEY_TYPE_ALIAS,
+                    KeyUtils.RSA_SHA256_CERT_TYPE_ALIAS,
+                    KeyUtils.RSA_SHA512_CERT_TYPE_ALIAS)
+                    .collect(Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER))));
+
     private int verifierSignatureSize = -1;
 
     protected SignatureRSA(String algorithm) {
@@ -64,7 +79,7 @@ public abstract class SignatureRSA extends AbstractSignature {
     @Override
     public boolean verify(SessionContext session, byte[] sig) throws Exception {
         byte[] data = sig;
-        Map.Entry<String, byte[]> encoding = extractEncodedSignature(data);
+        Map.Entry<String, byte[]> encoding = extractEncodedSignature(data, SUPPORTED_KEY_TYPES);
         if (encoding != null) {
             String keyType = encoding.getKey();
             /*
@@ -76,10 +91,7 @@ public abstract class SignatureRSA extends AbstractSignature {
              * another variant that corresponds to a good-faith implementation and is considered safe to accept.
              */
             String canonicalName = KeyUtils.getCanonicalKeyType(keyType);
-            if ((!KeyPairProvider.SSH_RSA.equals(canonicalName))
-                    && (!KeyPairProvider.SSH_RSA_CERT.equals(canonicalName))) {
-                throw new IllegalArgumentException("Mismatched key type: " + keyType);
-            }
+            ValidateUtils.checkTrue(SUPPORTED_KEY_TYPES.contains(canonicalName), "Mismatched key type: %s", keyType);
             data = encoding.getValue();
         }
 
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java
index 25dab72..a142196 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java
@@ -48,6 +48,7 @@ import org.apache.sshd.common.util.security.SecurityUtils;
 public final class EdDSASecurityProviderUtils {
     // See EdDSANamedCurveTable
     public static final String CURVE_ED25519_SHA512 = "Ed25519";
+    public static final int KEY_SIZE = 256;
 
     private EdDSASecurityProviderUtils() {
         throw new UnsupportedOperationException("No instance");
@@ -61,8 +62,12 @@ public final class EdDSASecurityProviderUtils {
         return EdDSAPrivateKey.class;
     }
 
+    public static boolean isEDDSAKey(Key key) {
+        return getEDDSAKeySize(key) == KEY_SIZE;
+    }
+
     public static int getEDDSAKeySize(Key key) {
-        return (SecurityUtils.isEDDSACurveSupported() && (key instanceof EdDSAKey)) ? 256 : -1;
+        return (SecurityUtils.isEDDSACurveSupported() && (key instanceof EdDSAKey)) ? KEY_SIZE : -1;
     }
 
     public static boolean compareEDDSAPPublicKeys(PublicKey k1, PublicKey k2) {
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/SignatureEd25519.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/SignatureEd25519.java
index 16d8cf8..f5dcba5 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/SignatureEd25519.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/SignatureEd25519.java
@@ -37,7 +37,8 @@ public class SignatureEd25519 extends AbstractSignature {
     @Override
     public boolean verify(SessionContext session, byte[] sig) throws Exception {
         byte[] data = sig;
-        Map.Entry<String, byte[]> encoding = extractEncodedSignature(data);
+        Map.Entry<String, byte[]> encoding
+                = extractEncodedSignature(data, k -> KeyPairProvider.SSH_ED25519.equalsIgnoreCase(k));
         if (encoding != null) {
             String keyType = encoding.getKey();
             ValidateUtils.checkTrue(
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserDecodingTest.java b/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserDecodingTest.java
index fd16ef2..a41cd93 100644
--- a/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserDecodingTest.java
+++ b/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserDecodingTest.java
@@ -91,6 +91,8 @@ public class OpenSSHKeyPairResourceParserDecodingTest extends OpenSSHKeyPairReso
                 fail("Unsupported key type name (" + pubName + "): " + supportedTypeNames);
             }
 
+            validateKeyPairSignable(pubName, kp);
+
             @SuppressWarnings("rawtypes")
             PrivateKeyEntryDecoder decoder = OpenSSHKeyPairResourceParser.getPrivateKeyEntryDecoder(prvKey);
             assertNotNull("No private key decoder", decoder);
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserPasswordTest.java b/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserPasswordTest.java
index 60acbce..b5033c4 100644
--- a/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserPasswordTest.java
+++ b/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserPasswordTest.java
@@ -163,6 +163,7 @@ public class OpenSSHKeyPairResourceParserPasswordTest extends OpenSSHKeyPairReso
             case RETRY:
                 assertEquals("Mismatched pairs count", 1, GenericUtils.size(pairs));
                 assertEquals("Mismatched retries count", MAX_RETRIES, retriesCount.getAndSet(0));
+                validateKeyPairSignable(resourceKey, GenericUtils.head(pairs));
                 break;
 
             default:
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParserTest.java b/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParserTest.java
index f078c23..4dbdcaf 100644
--- a/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParserTest.java
+++ b/sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParserTest.java
@@ -21,10 +21,12 @@ package org.apache.sshd.common.config.keys.loader.pem;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.net.URL;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.PrivateKey;
+import java.security.PublicKey;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -39,7 +41,9 @@ import org.apache.sshd.common.util.security.SecurityUtils;
 import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
 import org.apache.sshd.util.test.JUnitTestSupport;
 import org.apache.sshd.util.test.NoIoTestCase;
+import org.junit.Assume;
 import org.junit.FixMethodOrder;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
 import org.junit.runner.RunWith;
@@ -110,6 +114,38 @@ public class PKCS8PEMResourceKeyPairParserTest extends JUnitTestSupport {
         }
     }
 
+    // see https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b
+    // openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -pkeyopt ec_param_enc:named_curve -out
+    // pkcs8-ecdsa-256.pem
+    // openssl ecparam -genkey -name prime256v1 -noout -out pkcs8-ec-256.key
+    // openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in pkcs8-ec-256.key -out pkcs8-ec-256.pem
+
+    // openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:1024 -out pkcs8-rsa-1024.pem
+
+    // openssl asn1parse -inform PEM -in ...file... -dump
+    @Test // see SSHD-989
+    @Ignore("WIP")
+    public void testPKCS8FileParsing() throws Exception {
+        String resourceKey = "pkcs8-" + algorithm.toLowerCase() + "-" + keySize + ".pem";
+        URL url = getClass().getResource(resourceKey);
+        Assume.assumeTrue("No test file=" + resourceKey, url != null);
+
+        Collection<KeyPair> pairs = PKCS8PEMResourceKeyPairParser.INSTANCE.loadKeyPairs(null, url, null);
+        assertEquals("Mismatched extract keys count", 1, GenericUtils.size(pairs));
+
+        assertSignatureMatch("Cannot sign with recovered key pair", GenericUtils.head(pairs));
+    }
+
+    private static void assertSignatureMatch(String message, KeyPair kp) throws GeneralSecurityException {
+        assertSignatureMatch(message, kp.getPrivate(), kp.getPublic());
+    }
+
+    private static void assertSignatureMatch(
+            String message, PrivateKey privateKey, PublicKey publicKey)
+            throws GeneralSecurityException {
+        // TODO
+    }
+
     @Override
     public String toString() {
         return getClass().getSimpleName() + "[" + algorithm + "/" + keySize + "]";
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/signature/SignatureRSASHA1Test.java b/sshd-common/src/test/java/org/apache/sshd/common/signature/SignatureRSASHA1Test.java
index 9fb12dd..7bb3a15 100644
--- a/sshd-common/src/test/java/org/apache/sshd/common/signature/SignatureRSASHA1Test.java
+++ b/sshd-common/src/test/java/org/apache/sshd/common/signature/SignatureRSASHA1Test.java
@@ -118,7 +118,8 @@ public class SignatureRSASHA1Test extends JUnitTestSupport {
         assertTrue("Verifier signature size not initialized", vSize > 0);
 
         // make sure padding is required
-        Map.Entry<String, byte[]> encoding = rsa.extractEncodedSignature(TEST_SIGNATURE);
+        Map.Entry<String, byte[]> encoding = rsa.extractEncodedSignature(
+                TEST_SIGNATURE, SignatureRSA.SUPPORTED_KEY_TYPES);
         assertNotNull("Signature is not encoded", encoding);
         byte[] data = encoding.getValue();
         assertTrue("Signature data size (" + data.length + ") not below verifier size (" + vSize + ")", data.length < vSize);
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/util/security/SecurityUtilsTest.java b/sshd-common/src/test/java/org/apache/sshd/common/util/security/SecurityUtilsTest.java
index 2d7ea79..ecf7ac8 100644
--- a/sshd-common/src/test/java/org/apache/sshd/common/util/security/SecurityUtilsTest.java
+++ b/sshd-common/src/test/java/org/apache/sshd/common/util/security/SecurityUtilsTest.java
@@ -108,8 +108,7 @@ public class SecurityUtilsTest extends JUnitTestSupport {
         }
     }
 
-    private KeyPair testLoadEncryptedRSAPrivateKey(String algorithm)
-            throws IOException, GeneralSecurityException {
+    private KeyPair testLoadEncryptedRSAPrivateKey(String algorithm) throws Exception {
         return testLoadRSAPrivateKey(DEFAULT_PASSWORD.replace(' ', '-') + "-RSA-" + algorithm.toUpperCase() + "-key");
     }
 
@@ -140,17 +139,17 @@ public class SecurityUtilsTest extends JUnitTestSupport {
         }
     }
 
-    private KeyPair testLoadECPrivateKey(String name) throws IOException, GeneralSecurityException {
+    private KeyPair testLoadECPrivateKey(String name) throws Exception {
         return testLoadPrivateKey(name, ECPublicKey.class, ECPrivateKey.class);
     }
 
-    private KeyPair testLoadRSAPrivateKey(String name) throws IOException, GeneralSecurityException {
+    private KeyPair testLoadRSAPrivateKey(String name) throws Exception {
         return testLoadPrivateKey(name, RSAPublicKey.class, RSAPrivateKey.class);
     }
 
     private KeyPair testLoadPrivateKey(
             String name, Class<? extends PublicKey> pubType, Class<? extends PrivateKey> prvType)
-            throws IOException, GeneralSecurityException {
+            throws Exception {
         Path folder = getTestResourcesFolder();
         Path file = folder.resolve(name);
         KeyPair kpFile = testLoadPrivateKeyFile(file, pubType, prvType);
@@ -167,6 +166,7 @@ public class SecurityUtilsTest extends JUnitTestSupport {
         Package pkg = clazz.getPackage();
         KeyPair kpResource = testLoadPrivateKeyResource(pkg.getName().replace('.', '/') + "/" + name, pubType, prvType);
         assertTrue(name + ": Mismatched key file vs. resource values", KeyUtils.compareKeyPairs(kpFile, kpResource));
+        validateKeyPairSignable(name, kpResource);
         return kpResource;
     }
 
diff --git a/sshd-common/src/test/java/org/apache/sshd/util/test/CommonTestSupportUtils.java b/sshd-common/src/test/java/org/apache/sshd/util/test/CommonTestSupportUtils.java
index 43f635a..d39e76c 100644
--- a/sshd-common/src/test/java/org/apache/sshd/util/test/CommonTestSupportUtils.java
+++ b/sshd-common/src/test/java/org/apache/sshd/util/test/CommonTestSupportUtils.java
@@ -26,17 +26,25 @@ import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.channels.ByteChannel;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.DirectoryStream;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
 import java.security.CodeSource;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
 import java.security.ProtectionDomain;
+import java.security.PublicKey;
+import java.security.interfaces.DSAPrivateKey;
+import java.security.interfaces.ECKey;
+import java.security.interfaces.RSAPrivateKey;
 import java.security.spec.InvalidKeySpecException;
 import java.util.Arrays;
 import java.util.Collection;
@@ -45,9 +53,11 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicReference;
 
+import net.i2p.crypto.eddsa.EdDSAPrivateKey;
 import org.apache.sshd.common.Factory;
 import org.apache.sshd.common.cipher.ECCurves;
 import org.apache.sshd.common.config.keys.KeyUtils;
@@ -56,6 +66,8 @@ import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.keyprovider.KeyPairProviderHolder;
 import org.apache.sshd.common.random.Random;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.apache.sshd.common.signature.Signature;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.IoUtils;
@@ -532,7 +544,7 @@ public final class CommonTestSupportUtils {
         }
     }
 
-    private static <P extends KeyIdentityProvider> P validateKeyPairProvider(P provider) {
+    public static <P extends KeyIdentityProvider> P validateKeyPairProvider(P provider) {
         Objects.requireNonNull(provider, "No provider");
 
         // get the I/O out of the way
@@ -571,4 +583,76 @@ public final class CommonTestSupportUtils {
             return bytes;
         }
     }
+
+    /**
+     * Checks that the key pair can be used to successfully validate a signature
+     *
+     * @param  kp        The {@link KeyPair}
+     * @return           An {@link Optional} holding the verification result - if empty then no appropriate signer was
+     *                   found for the keys.
+     * @throws Exception If failed to generate the signature
+     */
+    public static Optional<Boolean> verifySignatureMatch(KeyPair kp) throws Exception {
+        return verifySignatureMatch(kp.getPrivate(), kp.getPublic());
+    }
+
+    public static Optional<Boolean> verifySignatureMatch(
+            PrivateKey privateKey, PublicKey publicKey)
+            throws Exception {
+        Objects.requireNonNull(privateKey, "No private key provided");
+        Objects.requireNonNull(publicKey, "No public key provided");
+
+        // Use check only the private key so we can detect if "mixed" keys are used by failing the verification
+        if (privateKey instanceof RSAPrivateKey) {
+            return Optional.of(verifySignatureMatch(privateKey, publicKey, BuiltinSignatures.rsa));
+        } else if (privateKey instanceof DSAPrivateKey) {
+            return Optional.of(verifySignatureMatch(privateKey, publicKey, BuiltinSignatures.dsa));
+        } else if (SecurityUtils.isECCSupported() && (privateKey instanceof ECKey)) {
+            ECCurves curve = ECCurves.fromECKey((ECKey) privateKey);
+            ValidateUtils.checkNotNull(curve, "Unsupported EC key: %s", privateKey);
+            switch (curve) {
+                case nistp256:
+                    return Optional.of(verifySignatureMatch(privateKey, publicKey, BuiltinSignatures.nistp256));
+                case nistp384:
+                    return Optional.of(verifySignatureMatch(privateKey, publicKey, BuiltinSignatures.nistp384));
+                case nistp521:
+                    return Optional.of(verifySignatureMatch(privateKey, publicKey, BuiltinSignatures.nistp521));
+                default: // ignore
+            }
+        } else if (SecurityUtils.isEDDSACurveSupported() && (privateKey instanceof EdDSAPrivateKey)) {
+            return Optional.of(verifySignatureMatch(privateKey, publicKey, BuiltinSignatures.ed25519));
+        }
+
+        return Optional.empty();
+    }
+
+    public static boolean verifySignatureMatch(
+            PrivateKey privateKey, PublicKey publicKey, Factory<? extends Signature> factory)
+            throws Exception {
+        Signature signer = factory.create();
+        signer.initSigner(null, privateKey);
+
+        byte[] msg = ("[" + privateKey + "][" + publicKey + "]@" + signer).getBytes(StandardCharsets.UTF_8);
+        signer.update(null, msg);
+        byte[] signature = signer.sign(null);
+
+        Signature verifier = factory.create();
+        verifier.initVerifier(null, publicKey);
+        verifier.update(null, msg);
+        return verifier.verify(null, signature);
+    }
+
+    // clears the sensitive data regardless of success/failure
+    public static void writeSensitiveDataToFile(Path file, byte[] sensitiveData)
+            throws IOException {
+        try (ByteChannel out = Files.newByteChannel(file,
+                StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
+            ByteBuffer buf = ByteBuffer.wrap(sensitiveData);
+            while (buf.hasRemaining()) {
+                out.write(buf);
+            }
+        } finally {
+            Arrays.fill(sensitiveData, (byte) 0);
+        }
+    }
 }
diff --git a/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java b/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
index 6db9657..aede6de 100644
--- a/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
+++ b/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
@@ -52,6 +52,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import java.util.function.BiPredicate;
 
@@ -440,6 +441,14 @@ public abstract class JUnitTestSupport extends Assert {
         }
     }
 
+    public static KeyPair validateKeyPairSignable(Object hint, KeyPair kp) throws Exception {
+        assertNotNull(hint + ": no key pair provided", kp);
+        Optional<Boolean> signable = CommonTestSupportUtils.verifySignatureMatch(kp);
+        // if no result then assume "OK"
+        assertTrue(hint + ": Failed to validate signature", signable.orElse(Boolean.TRUE));
+        return kp;
+    }
+
     public static String resolveEffectiveAlgorithm(String algorithm) {
         if (GenericUtils.isEmpty(algorithm)) {
             return algorithm;