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