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
The following commit(s) were added to refs/heads/master by this push: new d1b5beb [SSHD-989] Add support for parsing PKCS8 encoded ed25519 private key d1b5beb is described below commit d1b5bebaa9c531bfea5cf4905ab14a85a9f2d403 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Fri May 22 20:41:13 2020 +0300 [SSHD-989] Add support for parsing PKCS8 encoded ed25519 private key --- .../loader/pem/PKCS8PEMResourceKeyPairParser.java | 5 + .../eddsa/Ed25519PEMResourceKeyParser.java | 183 +++++++++++++++++++++ .../pem/PKCS8PEMResourceKeyPairParserTest.java | 11 +- .../common/config/keys/loader/pem/pkcs8-eddsa.pem | 3 + 4 files changed, 199 insertions(+), 3 deletions(-) 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 3207c38..2ef18aa 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 @@ -45,6 +45,7 @@ import org.apache.sshd.common.util.io.der.ASN1Object; import org.apache.sshd.common.util.io.der.ASN1Type; import org.apache.sshd.common.util.io.der.DERParser; import org.apache.sshd.common.util.security.SecurityUtils; +import org.apache.sshd.common.util.security.eddsa.Ed25519PEMResourceKeyParser; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> @@ -107,6 +108,10 @@ public class PKCS8PEMResourceKeyPairParser extends AbstractPEMResourceKeyPairPar try (DERParser parser = privateKeyBytes.createParser()) { kp = ECDSAPEMResourceKeyPairParser.parseECKeyPair(curve, parser); } + } else if (SecurityUtils.isEDDSACurveSupported() + && Ed25519PEMResourceKeyParser.ED25519_OID.endsWith(oid)) { + ASN1Object privateKeyBytes = pkcs8Info.getPrivateKeyBytes(); + kp = Ed25519PEMResourceKeyParser.decodeEd25519KeyPair(privateKeyBytes.getPureValueBytes()); } else { PrivateKey prvKey = decodePEMPrivateKeyPKCS8(oidAlgorithm, encBytes); PublicKey pubKey = ValidateUtils.checkNotNull(KeyUtils.recoverPublicKey(prvKey), diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PEMResourceKeyParser.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PEMResourceKeyParser.java new file mode 100644 index 0000000..b658340 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PEMResourceKeyParser.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.common.util.security.eddsa; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StreamCorruptedException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import net.i2p.crypto.eddsa.EdDSAKey; +import net.i2p.crypto.eddsa.EdDSAPrivateKey; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; +import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec; +import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec; +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.loader.pem.AbstractPEMResourceKeyPairParser; +import org.apache.sshd.common.session.SessionContext; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.io.NoCloseInputStream; +import org.apache.sshd.common.util.io.der.ASN1Object; +import org.apache.sshd.common.util.io.der.ASN1Type; +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> + */ +public class Ed25519PEMResourceKeyParser extends AbstractPEMResourceKeyPairParser { + // TODO find out how the markers really look like for now provide something + public static final String BEGIN_MARKER = "BEGIN EDDSA PRIVATE KEY"; + public static final List<String> BEGINNERS = Collections.unmodifiableList(Collections.singletonList(BEGIN_MARKER)); + + public static final String END_MARKER = "END EDDSA PRIVATE KEY"; + public static final List<String> ENDERS = Collections.unmodifiableList(Collections.singletonList(END_MARKER)); + + /** + * @see <A HREF="https://tools.ietf.org/html/rfc8410#section-3>RFC8412 section 3</A> + */ + public static final String ED25519_OID = "1.3.101.112"; + + public static final Ed25519PEMResourceKeyParser INSTANCE = new Ed25519PEMResourceKeyParser(); + + public Ed25519PEMResourceKeyParser() { + super(EdDSAKey.KEY_ALGORITHM, ED25519_OID, BEGINNERS, ENDERS); + } + + @Override + public Collection<KeyPair> extractKeyPairs( + SessionContext session, NamedResource resourceKey, String beginMarker, + String endMarker, FilePasswordProvider passwordProvider, + InputStream stream, Map<String, String> headers) + throws IOException, GeneralSecurityException { + KeyPair kp = parseEd25519KeyPair(stream, false); + return Collections.singletonList(kp); + } + + public static KeyPair parseEd25519KeyPair( + InputStream inputStream, boolean okToClose) + throws IOException, GeneralSecurityException { + try (DERParser parser = new DERParser(NoCloseInputStream.resolveInputStream(inputStream, okToClose))) { + return parseEd25519KeyPair(parser); + } + } + + /* + * See https://tools.ietf.org/html/rfc8410#section-7 + * + * SEQUENCE { + * INTEGER 0x00 (0 decimal) + * SEQUENCE { + * OBJECTIDENTIFIER 1.3.101.112 + * } + * OCTETSTRING keyData + * } + * + * NOTE: there is another variant that also has some extra parameters + * but it has the same "prefix" structure so we don't care + */ + public static KeyPair parseEd25519KeyPair(DERParser parser) throws IOException, GeneralSecurityException { + ASN1Object obj = parser.readObject(); + if (obj == null) { + throw new StreamCorruptedException("Missing version value"); + } + + BigInteger version = obj.asInteger(); + if (!BigInteger.ZERO.equals(version)) { + throw new StreamCorruptedException("Invalid version: " + version); + } + + obj = parser.readObject(); + if (obj == null) { + throw new StreamCorruptedException("Missing OID container"); + } + + ASN1Type objType = obj.getObjType(); + if (objType != ASN1Type.SEQUENCE) { + throw new StreamCorruptedException("Unexpected OID object type: " + objType); + } + + List<Integer> curveOid; + try (DERParser oidParser = obj.createParser()) { + obj = oidParser.readObject(); + if (obj == null) { + throw new StreamCorruptedException("Missing OID value"); + } + + curveOid = obj.asOID(); + } + + String oid = GenericUtils.join(curveOid, '.'); + // TODO modify if more curves supported + if (!ED25519_OID.equals(oid)) { + throw new StreamCorruptedException("Unsupported curve OID: " + oid); + } + + obj = parser.readObject(); + if (obj == null) { + throw new StreamCorruptedException("Missing key data"); + } + + return decodeEd25519KeyPair(obj.getValue()); + } + + public static KeyPair decodeEd25519KeyPair(byte[] keyData) throws IOException, GeneralSecurityException { + EdDSAPrivateKey privateKey = decodeEdDSAPrivateKey(keyData); + EdDSAPublicKey publicKey = EdDSASecurityProviderUtils.recoverEDDSAPublicKey(privateKey); + return new KeyPair(publicKey, privateKey); + } + + public static EdDSAPrivateKey decodeEdDSAPrivateKey(byte[] keyData) throws IOException, GeneralSecurityException { + try (DERParser parser = new DERParser(keyData)) { + ASN1Object obj = parser.readObject(); + if (obj == null) { + throw new StreamCorruptedException("Missing key data container"); + } + + ASN1Type objType = obj.getObjType(); + if (objType != ASN1Type.OCTET_STRING) { + throw new StreamCorruptedException("Mismatched key data container type: " + objType); + } + + return generateEdDSAPrivateKey(obj.getValue()); + } + } + + public static EdDSAPrivateKey generateEdDSAPrivateKey(byte[] seed) throws GeneralSecurityException { + if (!SecurityUtils.isEDDSACurveSupported()) { + throw new NoSuchAlgorithmException(SecurityUtils.EDDSA + " provider not supported"); + } + + EdDSAParameterSpec params = EdDSANamedCurveTable.getByName(EdDSASecurityProviderUtils.CURVE_ED25519_SHA512); + EdDSAPrivateKeySpec keySpec = new EdDSAPrivateKeySpec(seed, params); + KeyFactory factory = SecurityUtils.getKeyFactory(SecurityUtils.EDDSA); + return EdDSAPrivateKey.class.cast(factory.generatePrivate(keySpec)); + } +} 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 76b9224..ab8cf8c 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 @@ -68,7 +68,7 @@ public class PKCS8PEMResourceKeyPairParserTest extends JUnitTestSupport { this.keySize = keySize; } - @Parameters(name = "{0} / {1}") + @Parameters(name = "{0}-{1}") public static List<Object[]> parameters() { List<Object[]> params = new ArrayList<>(); for (Integer ks : RSA_SIZES) { @@ -87,6 +87,9 @@ public class PKCS8PEMResourceKeyPairParserTest extends JUnitTestSupport { params.add(new Object[] { KeyUtils.EC_ALGORITHM, curve.getKeySize() }); } } + if (SecurityUtils.isEDDSACurveSupported()) { + params.add(new Object[] { SecurityUtils.EDDSA, 0 }); + } return params; } @@ -96,8 +99,8 @@ public class PKCS8PEMResourceKeyPairParserTest extends JUnitTestSupport { if (keySize > 0) { generator.initialize(keySize); } - KeyPair kp = generator.generateKeyPair(); + KeyPair kp = generator.generateKeyPair(); try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { Collection<Object> items = new ArrayList<>(); PrivateKey prv1 = kp.getPrivate(); @@ -126,11 +129,13 @@ public class PKCS8PEMResourceKeyPairParserTest extends JUnitTestSupport { * 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 ed25519 -out pkcs8-ed25519.pem * openssl asn1parse -inform PEM -in ...file... -dump */ @Test // see SSHD-989 public void testPKCS8FileParsing() throws Exception { - String resourceKey = "pkcs8-" + algorithm.toLowerCase() + "-" + keySize + ".pem"; + String baseName = "pkcs8-" + algorithm.toLowerCase(); + String resourceKey = baseName + ((keySize > 0) ? "-" + keySize : "") + ".pem"; URL url = getClass().getResource(resourceKey); Assume.assumeTrue("No test file=" + resourceKey, url != null); diff --git a/sshd-common/src/test/resources/org/apache/sshd/common/config/keys/loader/pem/pkcs8-eddsa.pem b/sshd-common/src/test/resources/org/apache/sshd/common/config/keys/loader/pem/pkcs8-eddsa.pem new file mode 100644 index 0000000..c8e2f2e --- /dev/null +++ b/sshd-common/src/test/resources/org/apache/sshd/common/config/keys/loader/pem/pkcs8-eddsa.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIB5k1Srs4YLjjoqR05Nu9CeBMVA2CDGK37sqjIoTehL1 +-----END PRIVATE KEY-----