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 <[email protected]> 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;
