Browse Source

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.
pull/1429/head
Florian Zschocke 1 year ago
parent
commit
366a14f278

+ 64
- 6
src/main/java/com/gitblit/transport/ssh/FileKeyPairProvider.java View 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);

+ 51
- 8
src/main/java/com/gitblit/transport/ssh/SshDaemon.java View 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;
}
}
}

+ 134
- 0
src/test/java/com/gitblit/transport/ssh/FileKeyPairProviderTest.java View File

@@ -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());
}
}

Loading…
Cancel
Save