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 95b1635454515bde44702f928e99696568540ea3 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Fri May 1 18:24:38 2020 +0300 [SSHD-989] Implement ECDSA public key recovery from PKCS#8 encoded data --- CHANGES.md | 6 +- .../loader/pem/DSSPEMResourceKeyPairParser.java | 2 +- .../loader/pem/ECDSAPEMResourceKeyPairParser.java | 75 ++++++++++------------ .../loader/pem/PKCS8PEMResourceKeyPairParser.java | 60 ++++++----------- .../keys/loader/pem/PKCS8PrivateKeyInfo.java | 16 ++--- .../loader/pem/RSAPEMResourceKeyPairParser.java | 2 +- .../org/apache/sshd/common/util/GenericUtils.java | 2 +- .../pem/PKCS8PEMResourceKeyPairParserTest.java | 6 ++ .../apache/sshd/util/test/JUnitTestSupport.java | 1 + 9 files changed, 75 insertions(+), 95 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3fb3d27..8e644be 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -53,6 +53,10 @@ 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 \ No newline at end of file +* [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/DSSPEMResourceKeyPairParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/DSSPEMResourceKeyPairParser.java index ca4f04a..796bf60 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/DSSPEMResourceKeyPairParser.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/DSSPEMResourceKeyPairParser.java @@ -47,7 +47,7 @@ import org.apache.sshd.common.util.security.SecurityUtils; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> - * @see <a href="https://tools.ietf.org/html/rfc3279#section-2.3.2">RFC-3279 section 2.3.2</a> + * @see <a href="https://tools.ietf.org/html/rfc3279#section-2.3.2">RFC-3279 section 2.3.2</a> */ public class DSSPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairParser { // Not exactly according to standard but good enough 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 12b74f9..44d3d51 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 @@ -51,7 +51,7 @@ import org.apache.sshd.common.util.security.SecurityUtils; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> - * @see <a href="https://tools.ietf.org/html/rfc5915">RFC 5915</a> + * @see <a href="https://tools.ietf.org/html/rfc5915">RFC 5915</a> */ public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairParser { public static final String BEGIN_MARKER = "BEGIN EC PRIVATE KEY"; @@ -78,7 +78,23 @@ public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar FilePasswordProvider passwordProvider, InputStream stream, Map<String, String> headers) throws IOException, GeneralSecurityException { - Map.Entry<ECPublicKeySpec, ECPrivateKeySpec> spec = decodeECPrivateKeySpec(stream, false); + + KeyPair kp = parseECKeyPair(stream, false); + return Collections.singletonList(kp); + } + + public static KeyPair parseECKeyPair( + InputStream inputStream, boolean okToClose) + throws IOException, GeneralSecurityException { + try (DERParser parser = new DERParser(NoCloseInputStream.resolveInputStream(inputStream, okToClose))) { + return parseECKeyPair(parser); + } + } + + public static KeyPair parseECKeyPair(DERParser parser) + throws IOException, GeneralSecurityException { + ASN1Object sequence = parser.readObject(); + Map.Entry<ECPublicKeySpec, ECPrivateKeySpec> spec = decodeECPrivateKeySpec(sequence); if (!SecurityUtils.isECCSupported()) { throw new NoSuchProviderException("ECC not supported"); } @@ -86,8 +102,7 @@ public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar KeyFactory kf = SecurityUtils.getKeyFactory(KeyUtils.EC_ALGORITHM); ECPublicKey pubKey = (ECPublicKey) kf.generatePublic(spec.getKey()); ECPrivateKey prvKey = (ECPrivateKey) kf.generatePrivate(spec.getValue()); - KeyPair kp = new KeyPair(pubKey, prvKey); - return Collections.singletonList(kp); + return new KeyPair(pubKey, prvKey); } /** @@ -121,23 +136,11 @@ public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar * </CODE> * </PRE> * - * @param inputStream The {@link InputStream} containing the DER encoded data - * @param okToClose {@code true} if OK to close the DER stream once parsing complete + * @param sequence The {@link ASN1Object} sequence containing the DER encoded data * @return The decoded {@link SimpleImmutableEntry} of {@link ECPublicKeySpec} and * {@link ECPrivateKeySpec} * @throws IOException If failed to to decode the DER stream */ - public static SimpleImmutableEntry<ECPublicKeySpec, ECPrivateKeySpec> decodeECPrivateKeySpec( - InputStream inputStream, boolean okToClose) - throws IOException { - ASN1Object sequence; - try (DERParser parser = new DERParser(NoCloseInputStream.resolveInputStream(inputStream, okToClose))) { - sequence = parser.readObject(); - } - - return decodeECPrivateKeySpec(sequence); - } - public static SimpleImmutableEntry<ECPublicKeySpec, ECPrivateKeySpec> decodeECPrivateKeySpec(ASN1Object sequence) throws IOException { ASN1Type objType = (sequence == null) ? null : sequence.getObjType(); @@ -155,12 +158,8 @@ public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar /* * According to https://tools.ietf.org/html/rfc5915 - section 3 * - * ECPrivateKey ::= SEQUENCE { - * version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), - * privateKey OCTET STRING, - * parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, - * publicKey [1] BIT STRING OPTIONAL - * } + * ECPrivateKey ::= SEQUENCE { version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), privateKey OCTET + * STRING, parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, publicKey [1] BIT STRING OPTIONAL } */ ECPoint w = decodeECPublicKeyValue(curve, parser); ECPublicKeySpec pubSpec = new ECPublicKeySpec(w, prvSpec.getParams()); @@ -171,12 +170,8 @@ public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar /* * According to https://tools.ietf.org/html/rfc5915 - section 3 * - * ECPrivateKey ::= SEQUENCE { - * version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), - * privateKey OCTET STRING, - * parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, - * publicKey [1] BIT STRING OPTIONAL - * } + * ECPrivateKey ::= SEQUENCE { version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), privateKey OCTET STRING, + * parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, publicKey [1] BIT STRING OPTIONAL } */ public static final ECPrivateKeySpec decodeECPrivateKeySpec(DERParser parser) throws IOException { // see openssl asn1parse -inform PEM -in ...file... -dump @@ -188,8 +183,8 @@ public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar /* * According to https://tools.ietf.org/html/rfc5915 - section 3 * - * For this version of the document, it SHALL be set to ecPrivkeyVer1, - * which is of type INTEGER and whose value is one (1) + * For this version of the document, it SHALL be set to ecPrivkeyVer1, which is of type INTEGER and whose value + * is one (1) */ BigInteger version = versionObject.asInteger(); if (!BigInteger.ONE.equals(version)) { @@ -209,14 +204,11 @@ public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar /* * According to https://tools.ietf.org/html/rfc5915 - section 3 * - * parameters specifies the elliptic curve domain parameters - * associated to the private key. The type ECParameters is discussed - * in [RFC5480]. As specified in [RFC5480], only the namedCurve - * CHOICE is permitted. namedCurve is an object identifier that - * fully identifies the required values for a particular set of - * elliptic curve domain parameters. Though the ASN.1 indicates that - * the parameters field is OPTIONAL, implementations that conform to - * this document MUST always include the parameters field. + * parameters specifies the elliptic curve domain parameters associated to the private key. The type + * ECParameters is discussed in [RFC5480]. As specified in [RFC5480], only the namedCurve CHOICE is permitted. + * namedCurve is an object identifier that fully identifies the required values for a particular set of elliptic + * curve domain parameters. Though the ASN.1 indicates that the parameters field is OPTIONAL, implementations + * that conform to this document MUST always include the parameters field. */ ASN1Object paramsObject = parser.readObject(); if (paramsObject == null) { @@ -271,9 +263,8 @@ public class ECDSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar /* * According to https://tools.ietf.org/html/rfc5915 * - * Though the ASN.1 indicates publicKey is OPTIONAL, - * implementations that conform to this document SHOULD - * always include the publicKey field + * Though the ASN.1 indicates publicKey is OPTIONAL, implementations that conform to this document SHOULD always + * include the publicKey field */ try (DERParser dataParser = dataObject.createParser()) { ASN1Object pointData = dataParser.readObject(); diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java index 37e6694..f871a3a 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java @@ -27,8 +27,6 @@ import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Collection; import java.util.Collections; @@ -42,11 +40,13 @@ import org.apache.sshd.common.session.SessionContext; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.common.util.io.der.ASN1Object; +import org.apache.sshd.common.util.io.der.DERParser; import org.apache.sshd.common.util.security.SecurityUtils; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> - * @see <a href="https://tools.ietf.org/html/rfc5208">RFC 5208</A> + * @see <a href="https://tools.ietf.org/html/rfc5208">RFC 5208</A> */ public class PKCS8PEMResourceKeyPairParser extends AbstractPEMResourceKeyPairParser { // Not exactly according to standard but good enough @@ -75,8 +75,8 @@ public class PKCS8PEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar byte[] encBytes = IoUtils.toByteArray(stream); PKCS8PrivateKeyInfo pkcs8Info = new PKCS8PrivateKeyInfo(encBytes); return extractKeyPairs( - session, resourceKey, beginMarker, endMarker, - passwordProvider, encBytes, pkcs8Info, headers); + session, resourceKey, beginMarker, endMarker, + passwordProvider, encBytes, pkcs8Info, headers); } public Collection<KeyPair> extractKeyPairs( @@ -86,44 +86,22 @@ public class PKCS8PEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar PKCS8PrivateKeyInfo pkcs8Info, Map<String, String> headers) throws IOException, GeneralSecurityException { List<Integer> oidAlgorithm = pkcs8Info.getAlgorithmIdentifier(); - PrivateKey prvKey = decodePEMPrivateKeyPKCS8(oidAlgorithm, encBytes); - PublicKey pubKey = recoverPublicKey( - session, resourceKey, beginMarker, endMarker, - passwordProvider, headers, encBytes, pkcs8Info, prvKey); - ValidateUtils.checkNotNull(pubKey, - "Failed to recover public key of OID=%s", oidAlgorithm); - KeyPair kp = new KeyPair(pubKey, prvKey); - return Collections.singletonList(kp); - - } - - @SuppressWarnings("checkstyle:ParameterNumber") - protected PublicKey recoverPublicKey( - SessionContext session, NamedResource resourceKey, - String beginMarker, String endMarker, - FilePasswordProvider passwordProvider, - Map<String, String> headers, byte[] encBytes, - PKCS8PrivateKeyInfo pkcs8Info, PrivateKey privateKey) - throws IOException, GeneralSecurityException { - if (privateKey instanceof ECPrivateKey) { - return recoverECPublicKey( - session, resourceKey, beginMarker, endMarker, - passwordProvider, headers, encBytes, pkcs8Info, - (ECPrivateKey) privateKey); + String oid = GenericUtils.join(oidAlgorithm, '.'); + KeyPair kp; + if (SecurityUtils.isECCSupported() + && ECDSAPEMResourceKeyPairParser.ECDSA_OID.equals(oid)) { + ASN1Object privateKeyBytes = pkcs8Info.getPrivateKeyBytes(); + try (DERParser parser = privateKeyBytes.createParser()) { + kp = ECDSAPEMResourceKeyPairParser.parseECKeyPair(parser); + } + } else { + PrivateKey prvKey = decodePEMPrivateKeyPKCS8(oidAlgorithm, encBytes); + PublicKey pubKey = ValidateUtils.checkNotNull(KeyUtils.recoverPublicKey(prvKey), + "Failed to recover public key of OID=%s", oidAlgorithm); + kp = new KeyPair(pubKey, prvKey); } - return KeyUtils.recoverPublicKey(privateKey); - } - - @SuppressWarnings("checkstyle:ParameterNumber") - protected ECPublicKey recoverECPublicKey( - SessionContext session, NamedResource resourceKey, - String beginMarker, String endMarker, - FilePasswordProvider passwordProvider, - Map<String, String> headers, byte[] encBytes, - PKCS8PrivateKeyInfo pkcs8Info, ECPrivateKey privateKey) - throws IOException, GeneralSecurityException { - throw new NoSuchAlgorithmException("TODO: SSHD-976"); + return Collections.singletonList(kp); } public static PrivateKey decodePEMPrivateKeyPKCS8(List<Integer> oidAlgorithm, byte[] keyBytes) diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PrivateKeyInfo.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PrivateKeyInfo.java index 873afb2..48917a5 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PrivateKeyInfo.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PrivateKeyInfo.java @@ -50,7 +50,7 @@ import org.apache.sshd.common.util.io.der.DERParser; * </PRE> * * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> - * @see <a href="https://tools.ietf.org/html/rfc5208#section-5">RFC 5208 - section 5</a> + * @see <a href="https://tools.ietf.org/html/rfc5208#section-5">RFC 5208 - section 5</a> */ public class PKCS8PrivateKeyInfo /* TODO Cloneable */ { private BigInteger version; @@ -108,11 +108,11 @@ public class PKCS8PrivateKeyInfo /* TODO Cloneable */ { } /** - * Decodes the current information with the data from the provided encoding. - * <B>Note:</B> User should {@link #clear()} the current information before parsing + * Decodes the current information with the data from the provided encoding. <B>Note:</B> User should + * {@link #clear()} the current information before parsing * - * @param privateKeyInfo The {@link ASN1Object} encoding - * @throws IOException If failed to parse the encoding + * @param privateKeyInfo The {@link ASN1Object} encoding + * @throws IOException If failed to parse the encoding */ public void decode(ASN1Object privateKeyInfo) throws IOException { try (DERParser parser = privateKeyInfo.createParser()) { @@ -158,8 +158,8 @@ public class PKCS8PrivateKeyInfo /* TODO Cloneable */ { @Override public String toString() { return getClass().getSimpleName() - + "[version=" + getVersion() - + ", algorithmIdentifier=" + getAlgorithmIdentifier() - + "]"; + + "[version=" + getVersion() + + ", algorithmIdentifier=" + getAlgorithmIdentifier() + + "]"; } } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/RSAPEMResourceKeyPairParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/RSAPEMResourceKeyPairParser.java index d599afe..161a0ac 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/RSAPEMResourceKeyPairParser.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/RSAPEMResourceKeyPairParser.java @@ -48,7 +48,7 @@ import org.apache.sshd.common.util.security.SecurityUtils; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> - * @see <a href="https://tools.ietf.org/html/rfc3279#section-2.3.1">RFC-3279 section 2.3.1</a> + * @see <a href="https://tools.ietf.org/html/rfc3279#section-2.3.1">RFC-3279 section 2.3.1</a> */ public class RSAPEMResourceKeyPairParser extends AbstractPEMResourceKeyPairParser { // Not exactly according to standard but good enough diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java index 4ed0514..1bc8bfa 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java @@ -1015,7 +1015,7 @@ public final class GenericUtils { /** * The delegate Suppliers get() method is called exactly once and the result is cached. * - * @param <T> Generic type of supplied value + * @param <T> Generic type of supplied value * @param delegate The actual Supplier * @return The memoized Supplier */ 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 4c6fdd8..f078c23 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 @@ -32,6 +32,7 @@ import java.util.List; import org.apache.commons.ssl.PEMItem; import org.apache.commons.ssl.PEMUtil; import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.cipher.ECCurves; import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.security.SecurityUtils; @@ -74,6 +75,11 @@ public class PKCS8PEMResourceKeyPairParserTest extends JUnitTestSupport { for (Integer ks : DSS_SIZES) { params.add(new Object[] { KeyUtils.DSS_ALGORITHM, ks }); } + if (SecurityUtils.isECCSupported()) { + for (ECCurves curve : ECCurves.VALUES) { + params.add(new Object[] { KeyUtils.EC_ALGORITHM, curve.getKeySize() }); + } + } return params; } 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 84b58c8..6db9657 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 @@ -447,6 +447,7 @@ public abstract class JUnitTestSupport extends Assert { return KeyUtils.EC_ALGORITHM; } else { return algorithm.toUpperCase(Locale.ENGLISH); + } } public static void assertRSAPublicKeyEquals(String message, RSAPublicKey expected, RSAPublicKey actual) {