From 366a14f278095bb28956298bd8c3c64b247700cb Mon Sep 17 00:00:00 2001 From: Florian Zschocke Date: Mon, 24 Oct 2022 21:10:13 +0200 Subject: [PATCH] Add SSH host keys with ECDSA and Ed25519 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. --- .../transport/ssh/FileKeyPairProvider.java | 70 ++++++++- .../com/gitblit/transport/ssh/SshDaemon.java | 59 ++++++-- .../ssh/FileKeyPairProviderTest.java | 134 ++++++++++++++++++ 3 files changed, 249 insertions(+), 14 deletions(-) create mode 100644 src/test/java/com/gitblit/transport/ssh/FileKeyPairProviderTest.java diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyPairProvider.java b/src/main/java/com/gitblit/transport/ssh/FileKeyPairProvider.java index 4ee0fbcd..aaa606ce 100644 --- a/src/main/java/com/gitblit/transport/ssh/FileKeyPairProvider.java +++ b/src/main/java/com/gitblit/transport/ssh/FileKeyPairProvider.java @@ -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 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 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); diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index 7a31bc18..d4f1fab0 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -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 index 00000000..d36adc7f --- /dev/null +++ b/src/test/java/com/gitblit/transport/ssh/FileKeyPairProviderTest.java @@ -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 keyPairs = hostKeyPairProvider.loadKeys(); + Iterator 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 keyPairs = hostKeyPairProvider.loadKeys(); + Iterator 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 keyPairs = hostKeyPairProvider.loadKeys(); + Iterator 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 keyPairs = hostKeyPairProvider.loadKeys(); + Iterator 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 keyPairs = hostKeyPairProvider.loadKeys(); + Iterator 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()); + } +} -- 2.39.5