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
import java.io.FileInputStream; | import java.io.FileInputStream; | ||||
import java.io.InputStreamReader; | import java.io.InputStreamReader; | ||||
import java.security.KeyFactory; | |||||
import java.security.KeyPair; | import java.security.KeyPair; | ||||
import java.security.PrivateKey; | |||||
import java.security.PublicKey; | |||||
import java.util.Arrays; | import java.util.Arrays; | ||||
import java.util.Iterator; | import java.util.Iterator; | ||||
import java.util.NoSuchElementException; | import java.util.NoSuchElementException; | ||||
import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder; | |||||
import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider; | import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider; | ||||
import org.apache.sshd.common.util.security.SecurityUtils; | import org.apache.sshd.common.util.security.SecurityUtils; | ||||
import org.bouncycastle.openssl.PEMKeyPair; | import org.bouncycastle.openssl.PEMKeyPair; | ||||
import org.bouncycastle.openssl.PEMParser; | import org.bouncycastle.openssl.PEMParser; | ||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; | 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. | * This host key provider loads private keys from the specified files. | ||||
this.files = files; | this.files = files; | ||||
} | } | ||||
@Override | |||||
public Iterable<KeyPair> loadKeys() | public Iterable<KeyPair> loadKeys() | ||||
{ | { | ||||
if (!SecurityUtils.isBouncyCastleRegistered()) { | if (!SecurityUtils.isBouncyCastleRegistered()) { | ||||
}; | }; | ||||
} | } | ||||
protected KeyPair doLoadKey(String file) | |||||
private KeyPair doLoadKey(String file) | |||||
{ | { | ||||
try { | 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(); | Object o = r.readObject(); | ||||
JcaPEMKeyConverter pemConverter = new JcaPEMKeyConverter(); | JcaPEMKeyConverter pemConverter = new JcaPEMKeyConverter(); | ||||
else if (o instanceof KeyPair) { | else if (o instanceof KeyPair) { | ||||
return (KeyPair)o; | return (KeyPair)o; | ||||
} | } | ||||
else { | |||||
log.warn("Cannot read unsupported PEM object of type: " + o.getClass().getCanonicalName()); | |||||
} | |||||
} | } | ||||
finally { | |||||
r.close(); | |||||
} | |||||
} | } | ||||
catch (Exception e) { | catch (Exception e) { | ||||
log.warn("Unable to read key " + file, e); | log.warn("Unable to read key " + file, e); |
*/ | */ | ||||
package com.gitblit.transport.ssh; | package com.gitblit.transport.ssh; | ||||
import java.io.ByteArrayOutputStream; | |||||
import java.io.File; | import java.io.File; | ||||
import java.io.FileOutputStream; | import java.io.FileOutputStream; | ||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.net.InetSocketAddress; | import java.net.InetSocketAddress; | ||||
import java.security.KeyPair; | import java.security.KeyPair; | ||||
import java.security.KeyPairGenerator; | import java.security.KeyPairGenerator; | ||||
import java.security.PrivateKey; | |||||
import java.text.MessageFormat; | import java.text.MessageFormat; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.concurrent.atomic.AtomicBoolean; | 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.IoServiceFactoryFactory; | ||||
import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory; | import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory; | ||||
import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory; | import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory; | ||||
import org.apache.sshd.common.util.security.SecurityUtils; | import org.apache.sshd.common.util.security.SecurityUtils; | ||||
import org.apache.sshd.common.util.security.bouncycastle.BouncyCastleSecurityProviderRegistrar; | 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.EdDSASecurityProviderRegistrar; | ||||
import org.apache.sshd.common.util.security.eddsa.OpenSSHEd25519PrivateKeyEntryDecoder; | |||||
import org.apache.sshd.server.SshServer; | import org.apache.sshd.server.SshServer; | ||||
import org.apache.sshd.server.auth.pubkey.CachingPublicKeyAuthenticator; | 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.eclipse.jgit.internal.JGitText; | ||||
import org.slf4j.Logger; | import org.slf4j.Logger; | ||||
import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||
*/ | */ | ||||
public class SshDaemon { | 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_PUBLICKEY = "publickey"; | ||||
private static final String AUTH_PASSWORD = "password"; | private static final String AUTH_PASSWORD = "password"; | ||||
// Generate host RSA and DSA keypairs and create the host keypair provider | // Generate host RSA and DSA keypairs and create the host keypair provider | ||||
File rsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-rsa-hostkey.pem"); | File rsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-rsa-hostkey.pem"); | ||||
File dsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-dsa-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(rsaKeyStore, "RSA", 2048); | ||||
generateKeyPair(dsaKeyStore, "DSA", 0); | |||||
generateKeyPair(ecdsaKeyStore, "ECDSA", 256); | |||||
generateKeyPair(eddsaKeyStore, "EdDSA", 0); | |||||
FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider(); | 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 | // Configure the preferred SSHD backend | ||||
} | } | ||||
} | } | ||||
private void generateKeyPair(File file, String algorithm, int keySize) { | |||||
static void generateKeyPair(File file, String algorithm, int keySize) { | |||||
if (file.exists()) { | if (file.exists()) { | ||||
return; | return; | ||||
} | } | ||||
} | } | ||||
FileOutputStream os = new FileOutputStream(file); | 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.flush(); | ||||
w.close(); | w.close(); | ||||
} catch (Exception e) { | } catch (Exception e) { | ||||
log.warn(MessageFormat.format("Unable to generate {0} keypair", algorithm), e); | log.warn(MessageFormat.format("Unable to generate {0} keypair", algorithm), e); | ||||
return; | |||||
} | } | ||||
} | } | ||||
} | } |
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()); | |||||
} | |||||
} |