]> source.dussan.org Git - gitblit.git/commitdiff
Add SSH host keys with ECDSA and Ed25519
authorFlorian Zschocke <f.zschocke+git@gmail.com>
Mon, 24 Oct 2022 19:10:13 +0000 (21:10 +0200)
committerFlorian Zschocke <f.zschocke+git@gmail.com>
Mon, 24 Oct 2022 22:01:01 +0000 (00:01 +0200)
Create new host keys, one with ECDSA and one with Ed25519 algorithms.
For the Ed25519 currently the EdDSA library from i2p is used. This
requires some quirks, compared to a modern BouncyCastle. But the SSHD
library used cannot use BouncyCastle yet for Ed25519.

No DSA key is generated anymore, but we still support existing ones.

src/main/java/com/gitblit/transport/ssh/FileKeyPairProvider.java
src/main/java/com/gitblit/transport/ssh/SshDaemon.java
src/test/java/com/gitblit/transport/ssh/FileKeyPairProviderTest.java [new file with mode: 0644]

index 4ee0fbcdf87be05134049cf75808ee32e2a09d7b..aaa606ced24c0e85d3ee43e4cc7f0c537803344e 100644 (file)
@@ -20,16 +20,29 @@ package com.gitblit.transport.ssh;
 
 import java.io.FileInputStream;
 import java.io.InputStreamReader;
+import java.security.KeyFactory;
 import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 
+import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder;
 import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider;
 import org.apache.sshd.common.util.security.SecurityUtils;
 import org.bouncycastle.openssl.PEMKeyPair;
 import org.bouncycastle.openssl.PEMParser;
 import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
+import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
+import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
+import org.bouncycastle.crypto.util.OpenSSHPrivateKeyUtil;
+import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil;
+import org.bouncycastle.jcajce.spec.OpenSSHPrivateKeySpec;
+import org.bouncycastle.jcajce.spec.OpenSSHPublicKeySpec;
+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemReader;
 
 /**
  * This host key provider loads private keys from the specified files.
@@ -63,6 +76,8 @@ public class FileKeyPairProvider extends AbstractKeyPairProvider
         this.files = files;
     }
 
+
+    @Override
     public Iterable<KeyPair> loadKeys()
     {
         if (!SecurityUtils.isBouncyCastleRegistered()) {
@@ -121,11 +136,53 @@ public class FileKeyPairProvider extends AbstractKeyPairProvider
         };
     }
 
-    protected KeyPair doLoadKey(String file)
+
+    private KeyPair doLoadKey(String file)
     {
         try {
-            PEMParser r = new PEMParser(new InputStreamReader(new FileInputStream(file)));
-            try {
+
+            try (PemReader r = new PemReader(new InputStreamReader(new FileInputStream(file)))) {
+                PemObject pemObject = r.readPemObject();
+                if ("OPENSSH PRIVATE KEY".equals(pemObject.getType())) {
+                    // This reads a properly OpenSSH formatted ed25519 private key file.
+                    // It is currently unused because the SSHD library in play doesn't work with proper keys.
+                    // This is kept in the hope that in the future the library offers proper support.
+                    try {
+                        byte[] privateKeyContent = pemObject.getContent();
+                        AsymmetricKeyParameter privateKeyParameters = OpenSSHPrivateKeyUtil.parsePrivateKeyBlob(privateKeyContent);
+                        if (privateKeyParameters instanceof Ed25519PrivateKeyParameters) {
+                            OpenSSHPrivateKeySpec privkeySpec = new OpenSSHPrivateKeySpec(privateKeyContent);
+
+                            Ed25519PublicKeyParameters publicKeyParameters = ((Ed25519PrivateKeyParameters)privateKeyParameters).generatePublicKey();
+                            OpenSSHPublicKeySpec pubKeySpec = new OpenSSHPublicKeySpec(OpenSSHPublicKeyUtil.encodePublicKey(publicKeyParameters));
+
+                            KeyFactory kf = KeyFactory.getInstance("Ed25519", "BC");
+                            PrivateKey privateKey = kf.generatePrivate(privkeySpec);
+                            PublicKey publicKey = kf.generatePublic(pubKeySpec);
+                            return new KeyPair(publicKey, privateKey);
+                        }
+                        else {
+                            log.warn("OpenSSH format is only supported for Ed25519 key type. Unable to read key " + file);
+                        }
+                    }
+                    catch (Exception e) {
+                        log.warn("Unable to read key " + file, e);
+                    }
+                    return null;
+                }
+
+                if ("EDDSA PRIVATE KEY".equals(pemObject.getType())) {
+                    // This reads the ed25519 key from a file format that we created in SshDaemon.
+                    // The type EDDSA PRIVATE KEY was given by us and nothing official.
+                    byte[] privateKeyContent = pemObject.getContent();
+                    PrivateKeyEntryDecoder<? extends PublicKey,? extends PrivateKey> decoder = SecurityUtils.getOpenSSHEDDSAPrivateKeyEntryDecoder();
+                    PrivateKey privateKey = decoder.decodePrivateKey(null, privateKeyContent, 0, privateKeyContent.length);
+                    PublicKey publicKey = SecurityUtils. recoverEDDSAPublicKey(privateKey);
+                    return new KeyPair(publicKey, privateKey);
+                }
+            }
+
+            try (PEMParser r = new PEMParser(new InputStreamReader(new FileInputStream(file)))) {
                 Object o = r.readObject();
 
                 JcaPEMKeyConverter pemConverter = new JcaPEMKeyConverter();
@@ -137,10 +194,11 @@ public class FileKeyPairProvider extends AbstractKeyPairProvider
                 else if (o instanceof KeyPair) {
                     return (KeyPair)o;
                 }
+                else {
+                    log.warn("Cannot read unsupported PEM object of type: " + o.getClass().getCanonicalName());
+                }
             }
-            finally {
-                r.close();
-            }
+
         }
         catch (Exception e) {
             log.warn("Unable to read key " + file, e);
index 7a31bc186b595edf80575673952647ad247a71b8..d4f1fab019bc95498eac787a0e2c7c63ed8b196c 100644 (file)
@@ -15,6 +15,7 @@
  */
 package com.gitblit.transport.ssh;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -22,19 +23,28 @@ import java.io.OutputStreamWriter;
 import java.net.InetSocketAddress;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
 import java.text.MessageFormat;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import net.i2p.crypto.eddsa.EdDSAPrivateKey;
+import org.apache.sshd.common.config.keys.KeyEntryResolver;
 import org.apache.sshd.common.io.IoServiceFactoryFactory;
 import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory;
 import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory;
 import org.apache.sshd.common.util.security.SecurityUtils;
 import org.apache.sshd.common.util.security.bouncycastle.BouncyCastleSecurityProviderRegistrar;
 import org.apache.sshd.common.util.security.eddsa.EdDSASecurityProviderRegistrar;
+import org.apache.sshd.common.util.security.eddsa.OpenSSHEd25519PrivateKeyEntryDecoder;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.auth.pubkey.CachingPublicKeyAuthenticator;
-import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
+import org.bouncycastle.crypto.util.OpenSSHPrivateKeyUtil;
+import org.bouncycastle.crypto.util.PrivateKeyFactory;
+import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemWriter;
 import org.eclipse.jgit.internal.JGitText;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -56,7 +66,7 @@ import com.google.common.io.Files;
  */
 public class SshDaemon {
 
-       private final Logger log = LoggerFactory.getLogger(SshDaemon.class);
+       private static final Logger log = LoggerFactory.getLogger(SshDaemon.class);
 
        private static final String AUTH_PUBLICKEY = "publickey";
        private static final String AUTH_PASSWORD = "password";
@@ -107,10 +117,14 @@ public class SshDaemon {
                // Generate host RSA and DSA keypairs and create the host keypair provider
                File rsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-rsa-hostkey.pem");
                File dsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-dsa-hostkey.pem");
+               File ecdsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-ecdsa-hostkey.pem");
+               File eddsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-eddsa-hostkey.pem");
+               File ed25519KeyStore = new File(gitblit.getBaseFolder(), "ssh-ed25519-hostkey.pem");
                generateKeyPair(rsaKeyStore, "RSA", 2048);
-               generateKeyPair(dsaKeyStore, "DSA", 0);
+               generateKeyPair(ecdsaKeyStore, "ECDSA", 256);
+               generateKeyPair(eddsaKeyStore, "EdDSA", 0);
                FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
-               hostKeyPairProvider.setFiles(new String [] { rsaKeyStore.getPath(), dsaKeyStore.getPath(), dsaKeyStore.getPath() });
+               hostKeyPairProvider.setFiles(new String [] { ecdsaKeyStore.getPath(), eddsaKeyStore.getPath(), ed25519KeyStore.getPath(), rsaKeyStore.getPath(), dsaKeyStore.getPath() });
 
 
                // Configure the preferred SSHD backend
@@ -244,7 +258,7 @@ public class SshDaemon {
                }
        }
 
-    private void generateKeyPair(File file, String algorithm, int keySize) {
+    static void generateKeyPair(File file, String algorithm, int keySize) {
        if (file.exists()) {
                return;
        }
@@ -267,13 +281,42 @@ public class SshDaemon {
             }
 
             FileOutputStream os = new FileOutputStream(file);
-            JcaPEMWriter w = new JcaPEMWriter(new OutputStreamWriter(os));
-            w.writeObject(kp);
+            PemWriter w = new PemWriter(new OutputStreamWriter(os));
+            if (algorithm.equals("ED25519")) {
+               // This generates a proper OpenSSH formatted ed25519 private key file.
+                               // It is currently unused because the SSHD library in play doesn't work with proper keys.
+                               // This is kept in the hope that in the future the library offers proper support.
+               AsymmetricKeyParameter keyParam = PrivateKeyFactory.createKey(kp.getPrivate().getEncoded());
+                               byte[] encKey = OpenSSHPrivateKeyUtil.encodePrivateKey(keyParam);
+                               w.writeObject(new PemObject("OPENSSH PRIVATE KEY", encKey));
+                       }
+                       else if (algorithm.equals("EdDSA")) {
+                               // This saves the ed25519 key in a file format that the current SSHD library can work with.
+                               // We call it EDDSA PRIVATE KEY, but that string is given by us and nothing official.
+                               PrivateKey privateKey = kp.getPrivate();
+                               if (privateKey instanceof EdDSAPrivateKey) {
+                                       OpenSSHEd25519PrivateKeyEntryDecoder encoder = (OpenSSHEd25519PrivateKeyEntryDecoder)SecurityUtils.getOpenSSHEDDSAPrivateKeyEntryDecoder();
+                                       EdDSAPrivateKey dsaPrivateKey = (EdDSAPrivateKey)privateKey;
+                                       // Jumping through some hoops here, because the decoder expects the key type as a string at the
+                                       // start, but the encoder doesn't put it in. So we have to put it in ourselves.
+                                       ByteArrayOutputStream encos = new ByteArrayOutputStream();
+                                       String type = encoder.encodePrivateKey(encos, dsaPrivateKey);
+                                       ByteArrayOutputStream bos = new ByteArrayOutputStream();
+                                       KeyEntryResolver.encodeString(bos, type);
+                                       encos.writeTo(bos);
+                                       w.writeObject(new PemObject("EDDSA PRIVATE KEY", bos.toByteArray()));
+                               }
+                               else {
+                                       log.warn("Unable to encode EdDSA key, got key type " + privateKey.getClass().getCanonicalName());
+                               }
+                       }
+            else {
+                               w.writeObject(new JcaMiscPEMGenerator(kp));
+                       }
             w.flush();
             w.close();
         } catch (Exception e) {
             log.warn(MessageFormat.format("Unable to generate {0} keypair", algorithm), e);
-            return;
         }
     }
 }
diff --git a/src/test/java/com/gitblit/transport/ssh/FileKeyPairProviderTest.java b/src/test/java/com/gitblit/transport/ssh/FileKeyPairProviderTest.java
new file mode 100644 (file)
index 0000000..d36adc7
--- /dev/null
@@ -0,0 +1,134 @@
+package com.gitblit.transport.ssh;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.KeyPair;
+import java.util.Iterator;
+
+import static org.junit.Assert.*;
+
+public class FileKeyPairProviderTest
+{
+    @Rule
+    public TemporaryFolder testFolder = new TemporaryFolder();
+
+    private void generateKeyPair(File file, String algorithm, int keySize) {
+        if (file.exists()) {
+            file.delete();
+        }
+        SshDaemon.generateKeyPair(file, algorithm, keySize);
+    }
+
+    @Test
+    public void loadKeysEddsa() throws IOException
+    {
+        File file = testFolder.newFile("eddsa.pem");
+        generateKeyPair(file, "EdDSA", 0);
+
+        FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
+        hostKeyPairProvider.setFiles(new String [] { file.getPath() });
+
+        Iterable<KeyPair> keyPairs = hostKeyPairProvider.loadKeys();
+        Iterator<KeyPair> iterator = keyPairs.iterator();
+        assertTrue(iterator.hasNext());
+        KeyPair keyPair = iterator.next();
+        assertNotNull(keyPair);
+        assertEquals("Unexpected key pair type", "EdDSA", keyPair.getPrivate().getAlgorithm());
+    }
+
+    @Test
+    public void loadKeysEd25519() throws IOException
+    {
+        File file = testFolder.newFile("ed25519.pem");
+        generateKeyPair(file, "ED25519", 0);
+
+        FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
+        hostKeyPairProvider.setFiles(new String [] { file.getPath() });
+
+        Iterable<KeyPair> keyPairs = hostKeyPairProvider.loadKeys();
+        Iterator<KeyPair> iterator = keyPairs.iterator();
+        assertTrue(iterator.hasNext());
+        KeyPair keyPair = iterator.next();
+        assertNotNull(keyPair);
+        assertEquals("Unexpected key pair type", "Ed25519", keyPair.getPrivate().getAlgorithm());
+    }
+
+    @Test
+    public void loadKeysECDSA() throws IOException
+    {
+        File file = testFolder.newFile("ecdsa.pem");
+        generateKeyPair(file, "ECDSA", 0);
+
+        FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
+        hostKeyPairProvider.setFiles(new String [] { file.getPath() });
+
+        Iterable<KeyPair> keyPairs = hostKeyPairProvider.loadKeys();
+        Iterator<KeyPair> iterator = keyPairs.iterator();
+        assertTrue(iterator.hasNext());
+        KeyPair keyPair = iterator.next();
+        assertNotNull(keyPair);
+        assertEquals("Unexpected key pair type", "ECDSA", keyPair.getPrivate().getAlgorithm());
+    }
+
+    @Test
+    public void loadKeysRSA() throws IOException
+    {
+        File file = testFolder.newFile("rsa.pem");
+        generateKeyPair(file, "RSA", 4096);
+
+        FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
+        hostKeyPairProvider.setFiles(new String [] { file.getPath() });
+
+        Iterable<KeyPair> keyPairs = hostKeyPairProvider.loadKeys();
+        Iterator<KeyPair> iterator = keyPairs.iterator();
+        assertTrue(iterator.hasNext());
+        KeyPair keyPair = iterator.next();
+        assertNotNull(keyPair);
+        assertEquals("Unexpected key pair type", "RSA", keyPair.getPrivate().getAlgorithm());
+    }
+
+    @Test
+    public void loadKeysDefault() throws IOException
+    {
+        File rsa = testFolder.newFile("rsa.pem");
+        generateKeyPair(rsa, "RSA", 2048);
+        File ecdsa = testFolder.newFile("ecdsa.pem");
+        generateKeyPair(ecdsa, "ECDSA", 0);
+        File eddsa = testFolder.newFile("eddsa.pem");
+        generateKeyPair(eddsa, "EdDSA", 0);
+        File ed25519 = testFolder.newFile("ed25519.pem");
+        generateKeyPair(ed25519, "ED25519", 0);
+
+        FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
+        hostKeyPairProvider.setFiles(new String [] { ecdsa.getPath(), eddsa.getPath(), rsa.getPath(), ed25519.getPath() });
+
+        Iterable<KeyPair> keyPairs = hostKeyPairProvider.loadKeys();
+        Iterator<KeyPair> iterator = keyPairs.iterator();
+
+        assertTrue(iterator.hasNext());
+        KeyPair keyPair = iterator.next();
+        assertNotNull(keyPair);
+        assertEquals("Unexpected key pair type", "ECDSA", keyPair.getPrivate().getAlgorithm());
+
+        assertTrue(iterator.hasNext());
+        keyPair = iterator.next();
+        assertNotNull(keyPair);
+        assertEquals("Unexpected key pair type", "EdDSA", keyPair.getPrivate().getAlgorithm());
+
+        assertTrue(iterator.hasNext());
+        keyPair = iterator.next();
+        assertNotNull(keyPair);
+        assertEquals("Unexpected key pair type", "RSA", keyPair.getPrivate().getAlgorithm());
+
+        assertTrue(iterator.hasNext());
+        keyPair = iterator.next();
+        assertNotNull(keyPair);
+        assertEquals("Unexpected key pair type", "Ed25519", keyPair.getPrivate().getAlgorithm());
+
+        assertFalse(iterator.hasNext());
+    }
+}