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 17:48:02 UTC

[mina-sshd] branch master updated: [SSHD-989] Add support for parsing PKCS8 encoded ed25519 private key

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 <lg...@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:dev@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:dev@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-----