diff --git a/pom.xml b/pom.xml
index babeb493..87c6e68f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,11 +49,27 @@
4.10
test
+
+ net.i2p.crypto
+ eddsa
+ 0.2.0-SNAPSHOT
+
src
test
+
+
+ test
+
+ **
+
+
+ **/*.java
+
+
+
maven-release-plugin
diff --git a/src/com/trilead/ssh2/crypto/CertificateDecoder.java b/src/com/trilead/ssh2/crypto/CertificateDecoder.java
index 0dbd1f31..1eebe37d 100644
--- a/src/com/trilead/ssh2/crypto/CertificateDecoder.java
+++ b/src/com/trilead/ssh2/crypto/CertificateDecoder.java
@@ -12,5 +12,9 @@ public abstract class CertificateDecoder {
public abstract String getEndLine();
+ public KeyPair createKeyPair(PEMStructure pemStructure, String password) throws IOException {
+ return createKeyPair(pemStructure);
+ }
+
protected abstract KeyPair createKeyPair(PEMStructure pemStructure) throws IOException;
}
diff --git a/src/com/trilead/ssh2/crypto/PEMDecoder.java b/src/com/trilead/ssh2/crypto/PEMDecoder.java
index 1dfac3e7..95081bf1 100644
--- a/src/com/trilead/ssh2/crypto/PEMDecoder.java
+++ b/src/com/trilead/ssh2/crypto/PEMDecoder.java
@@ -5,15 +5,9 @@
import java.io.CharArrayReader;
import java.io.IOException;
import java.math.BigInteger;
-import java.security.GeneralSecurityException;
-import java.security.KeyFactory;
import java.security.KeyPair;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.spec.DSAPrivateKeySpec;
-import java.security.spec.DSAPublicKeySpec;
-import java.security.spec.RSAPrivateKeySpec;
-import java.security.spec.RSAPublicKeySpec;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import com.trilead.ssh2.crypto.cipher.AES;
import com.trilead.ssh2.crypto.cipher.BlockCipher;
@@ -34,6 +28,7 @@
*/
public class PEMDecoder
{
+ private static final Logger LOGGER = Logger.getLogger(PEMDecoder.class.getName());
private static final int PEM_RSA_PRIVATE_KEY = 1;
private static final int PEM_DSA_PRIVATE_KEY = 2;
@@ -78,7 +73,7 @@ private static byte[] hexToByteArray(String hex)
return decoded;
}
- private static byte[] generateKeyFromPasswordSaltWithMD5(byte[] password, byte[] salt, int keyLen)
+ public static byte[] generateKeyFromPasswordSaltWithMD5(byte[] password, byte[] salt, int keyLen)
throws IOException
{
if (salt.length < 8)
@@ -495,8 +490,9 @@ public static KeyPair decodeKeyPair(char[] pem, String password) throws IOExcept
}
try {
- return decoder.createKeyPair(ps);
+ return decoder.createKeyPair(ps, password);
} catch (IOException ex) {
+ LOGGER.log(Level.WARNING, "Could not decode PEM Key using current decoder", ex);
// we couldn't decode the input, try another decoder
}
}
diff --git a/src/com/trilead/ssh2/signature/ED25519KeyAlgorithm.java b/src/com/trilead/ssh2/signature/ED25519KeyAlgorithm.java
new file mode 100644
index 00000000..b46b53d7
--- /dev/null
+++ b/src/com/trilead/ssh2/signature/ED25519KeyAlgorithm.java
@@ -0,0 +1,195 @@
+package com.trilead.ssh2.signature;
+
+import com.trilead.ssh2.crypto.CertificateDecoder;
+import com.trilead.ssh2.crypto.PEMStructure;
+import com.trilead.ssh2.packets.TypesReader;
+import com.trilead.ssh2.packets.TypesWriter;
+import net.i2p.crypto.eddsa.EdDSAPrivateKey;
+import net.i2p.crypto.eddsa.EdDSAPublicKey;
+import net.i2p.crypto.eddsa.EdDSASecurityProvider;
+import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
+import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
+import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
+import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Arrays;
+
+/**
+ * @author Michael Clarke
+ */
+public class ED25519KeyAlgorithm extends KeyAlgorithm {
+
+ private static final String ED25519 = "ssh-ed25519";
+
+ protected ED25519KeyAlgorithm() {
+ /*Whilst the signature is 'NoneWith', it actually uses a digest from the key's parameter specification
+ * so is really SHA512WithEdDSA, but has to be looked up using what's in the Provider implementation.
+ */
+ super("NoneWithEdDSA", ED25519, EdDSAPrivateKey.class, new EdDSASecurityProvider());
+ }
+
+ @Override
+ public byte[] encodeSignature(byte[] signature) throws IOException {
+ TypesWriter signatureWriter = new TypesWriter();
+ signatureWriter.writeString(ED25519);
+ signatureWriter.writeString(signature, 0, signature.length);
+ return signatureWriter.getBytes();
+ }
+
+ @Override
+ public byte[] decodeSignature(byte[] encodedSignature) throws IOException {
+ TypesReader typesReader = new TypesReader(encodedSignature);
+
+ String signatureFormat = typesReader.readString();
+ if (!signatureFormat.equals(ED25519)) {
+ throw new IOException("Invalid signature format");
+ }
+
+ byte[] signature = typesReader.readByteString();
+ if (typesReader.remain() != 0) {
+ throw new IOException("Unexpected padding in signature");
+ }
+
+ return signature;
+ }
+
+ @Override
+ public byte[] encodePublicKey(EdDSAPublicKey publicKey) throws IOException {
+ byte[] encoded = publicKey.getAbyte();
+
+ TypesWriter typesWriter = new TypesWriter();
+ typesWriter.writeString(ED25519);
+ typesWriter.writeString(encoded, 0, encoded.length);
+ return typesWriter.getBytes();
+ }
+
+ @Override
+ public EdDSAPublicKey decodePublicKey(byte[] encodedPublicKey) throws IOException {
+ TypesReader typesReader = new TypesReader(encodedPublicKey);
+
+ String keyFormat = typesReader.readString();
+ if (!keyFormat.equals(ED25519)) {
+ throw new IOException("Invalid key type");
+ }
+
+ byte[] keyBytes = typesReader.readByteString();
+ if (0 != typesReader.remain()) {
+ throw new IOException("Unexpected padding in public key");
+ }
+
+ return new EdDSAPublicKey(new EdDSAPublicKeySpec(
+ keyBytes, EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)));
+ }
+
+ @Override
+ public CertificateDecoder getCertificateDecoder() {
+ return new OpenSshCertificateDecoder();
+ }
+
+ private static class OpenSshCertificateDecoder extends CertificateDecoder {
+
+ @Override
+ public String getStartLine() {
+ return "-----BEGIN OPENSSH PRIVATE KEY-----";
+ }
+
+ @Override
+ public String getEndLine() {
+ return "-----END OPENSSH PRIVATE KEY-----";
+ }
+
+ @Override
+ public KeyPair createKeyPair(PEMStructure pemStructure) {
+ return null;
+ }
+
+ @Override
+ public KeyPair createKeyPair(PEMStructure pemStructure, String password) throws IOException {
+ TypesReader pemReader = new TypesReader(pemStructure.getData());
+
+ byte[] header = pemReader.readBytes(15);
+ if (!"openssh-key-v1".equals(new String(header, StandardCharsets.UTF_8).trim())) {
+ throw new IOException("Could not find openssh header in key");
+ }
+
+ String cipher = pemReader.readString();
+ String kdf = pemReader.readString();
+ /*byte[] kdfOptions = */pemReader.readByteString(); //not used until we get key decryption sorted
+ int keyCount = pemReader.readUINT32();
+
+ // I can't actually find any test cases for multiple keys to know how they're used
+ if (keyCount != 1) {
+ throw new IOException("Only single OpenSSH keys are supported");
+ }
+
+ /*byte[] publicKeys = */pemReader.readByteString(); //public keys are also stored with each private key, so ignored here and parsed later
+ byte[] privateKeys = pemReader.readByteString();
+
+ if ("bcrypt".equals(kdf)) {
+ if (password == null) {
+ throw new IOException("PEM is encrypted but password has not been specified");
+ }
+
+ //TODO handle encrypted keys
+ /*
+ https://github.com/openssh/openssh-portable/blob/master/openbsd-compat/bcrypt_pbkdf.c
+ indicates open-ssh deviates from the normal implementation of bcrypt, so I'll need to check
+ whether I can find a Java library that supports this, or whether I have to modify an
+ existing implementation... or even write my own.
+ */
+ throw new IOException("Encrypted OpenSSH keys are not currently supported");
+ } else if (!"none".equals(cipher) || !"none".equals(kdf)) {
+ throw new IOException("Unexpected encryption method for key");
+ }
+
+ TypesReader privateKeyTypeReader = new TypesReader(privateKeys);
+ int checkNumber1 = privateKeyTypeReader.readUINT32();
+ int checkNumber2 = privateKeyTypeReader.readUINT32();
+
+ if (checkNumber1 != checkNumber2) {
+ throw new IOException("Check integers didn't match");
+ }
+
+ String keyType = privateKeyTypeReader.readString();
+ if (!keyType.equals(ED25519)) {
+ throw new IOException("Invalid key type");
+ }
+
+ byte[] publicBytes = privateKeyTypeReader.readByteString();
+ byte[] privateBytes = privateKeyTypeReader.readByteString();
+ EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512);
+
+ EdDSAPublicKeySpec publicKeySpec = new EdDSAPublicKeySpec(publicBytes, spec);
+ EdDSAPrivateKeySpec privateKeySpec = new EdDSAPrivateKeySpec(Arrays.copyOfRange(privateBytes, 0, 32), spec);
+
+ try {
+ KeyFactory factory = KeyFactory.getInstance("EdDSA", new EdDSASecurityProvider());
+ PublicKey publicKey = factory.generatePublic(publicKeySpec);
+ PrivateKey privateKey = factory.generatePrivate(privateKeySpec);
+
+
+ /*byte[] comment = */privateKeyTypeReader.readByteString(); // we don't need the key name/comment
+
+ for (int i = 0; i < pemReader.remain(); i++) {
+ if (i + 1 != pemReader.readByte()) {
+ throw new IOException("Incorrect padding on private keys");
+ }
+ }
+
+ return new KeyPair(publicKey, privateKey);
+ } catch (GeneralSecurityException ex) {
+ throw new IOException("Could not create EcDSA key pair", ex);
+ }
+ }
+
+
+ }
+
+}
diff --git a/src/com/trilead/ssh2/signature/KeyAlgorithm.java b/src/com/trilead/ssh2/signature/KeyAlgorithm.java
index 7619fac5..7eeb0d98 100644
--- a/src/com/trilead/ssh2/signature/KeyAlgorithm.java
+++ b/src/com/trilead/ssh2/signature/KeyAlgorithm.java
@@ -5,6 +5,7 @@
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
+import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
@@ -17,28 +18,34 @@ public abstract class KeyAlgorithm {
private final String signatureAlgorithm;
private final String keyFormat;
private final Class keyType;
+ private final Provider provider;
protected KeyAlgorithm(String signatureAlgorithm, String keyFormat, Class keyType) {
+ this(signatureAlgorithm, keyFormat, keyType, null);
+ }
+
+ protected KeyAlgorithm(String signatureAlgorithm, String keyFormat, Class keyType, Provider provider) {
super();
this.signatureAlgorithm = signatureAlgorithm;
this.keyFormat = keyFormat;
this.keyType = keyType;
+ this.provider = provider;
}
public byte[] generateSignature(byte[] message, R pk, SecureRandom rnd) throws IOException {
try {
- Signature signature = Signature.getInstance(signatureAlgorithm);
+ Signature signature = (null == provider ? Signature.getInstance(signatureAlgorithm) : Signature.getInstance(signatureAlgorithm, provider));
signature.initSign(pk, rnd);
signature.update(message);
return signature.sign();
} catch (GeneralSecurityException ex) {
- throw new IOException("Could not generate signature");
+ throw new IOException("Could not generate signature", ex);
}
}
public boolean verifySignature(byte[] message, byte[] ds, U dpk) throws IOException {
try {
- Signature signature = Signature.getInstance(signatureAlgorithm);
+ Signature signature = (null == provider ? Signature.getInstance(signatureAlgorithm) : Signature.getInstance(signatureAlgorithm, provider));
signature.initVerify(dpk);
signature.update(message);
return signature.verify(ds);
diff --git a/src/com/trilead/ssh2/signature/KeyAlgorithmManager.java b/src/com/trilead/ssh2/signature/KeyAlgorithmManager.java
index 641c141c..ff25d687 100644
--- a/src/com/trilead/ssh2/signature/KeyAlgorithmManager.java
+++ b/src/com/trilead/ssh2/signature/KeyAlgorithmManager.java
@@ -25,6 +25,7 @@ public static Collection> getSupportedAlgori
private static Collection> buildSupportAlgorithmsList() {
List> algorithms = new ArrayList<>();
+ algorithms.add(new ED25519KeyAlgorithm());
algorithms.add(new RSAKeyAlgorithm());
algorithms.add(new DSAKeyAlgorithm());
diff --git a/test/com/trilead/ssh2/signature/ED25519KeyAlgorithmTest.java b/test/com/trilead/ssh2/signature/ED25519KeyAlgorithmTest.java
new file mode 100644
index 00000000..5a4627f0
--- /dev/null
+++ b/test/com/trilead/ssh2/signature/ED25519KeyAlgorithmTest.java
@@ -0,0 +1,79 @@
+package com.trilead.ssh2.signature;
+
+import com.trilead.ssh2.crypto.PEMDecoder;
+import net.i2p.crypto.eddsa.EdDSAPrivateKey;
+import net.i2p.crypto.eddsa.EdDSAPublicKey;
+import net.i2p.crypto.eddsa.EdDSASecurityProvider;
+import org.apache.commons.io.IOUtils;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Michael Clarke
+ */
+public class ED25519KeyAlgorithmTest {
+
+ @Test
+ public void testEncodeDecodePublicKey() throws GeneralSecurityException, IOException {
+ ED25519KeyAlgorithm testCase = new ED25519KeyAlgorithm();
+ KeyPairGenerator factory = KeyPairGenerator.getInstance("EdDSA", new EdDSASecurityProvider());
+ EdDSAPublicKey publicKey = (EdDSAPublicKey) factory.generateKeyPair().getPublic();
+ byte[] encoded = testCase.encodePublicKey(publicKey);
+ EdDSAPublicKey decoded = testCase.decodePublicKey(encoded);
+ assertEquals(publicKey, decoded);
+ }
+
+ @Test
+ public void testEncodeDecodeSignature() throws GeneralSecurityException, IOException {
+ ED25519KeyAlgorithm testCase = new ED25519KeyAlgorithm();
+ KeyPairGenerator factory = KeyPairGenerator.getInstance("EdDSA", new EdDSASecurityProvider());
+ EdDSAPrivateKey privateKey = (EdDSAPrivateKey) factory.generateKeyPair().getPrivate();
+ byte[] signature = testCase.generateSignature("Sign Me".getBytes(StandardCharsets.UTF_8), privateKey, new SecureRandom());
+ byte[] encoded = testCase.encodeSignature(signature);
+ byte[] decoded = testCase.decodeSignature(encoded);
+ assertArrayEquals(signature, decoded);
+ }
+
+ @Test
+ public void testSignAndVerify() throws GeneralSecurityException, IOException {
+ ED25519KeyAlgorithm testCase = new ED25519KeyAlgorithm();
+ byte[] message = "Signature Test".getBytes(StandardCharsets.UTF_8);
+ KeyPairGenerator factory = KeyPairGenerator.getInstance("EdDSA", new EdDSASecurityProvider());
+ KeyPair keyPair = factory.generateKeyPair();
+ EdDSAPrivateKey privateKey = (EdDSAPrivateKey) keyPair.getPrivate();
+ EdDSAPublicKey publicKey = (EdDSAPublicKey) keyPair.getPublic();
+ byte[] signature = testCase.generateSignature(message, privateKey, new SecureRandom());
+ assertTrue(testCase.verifySignature(message, signature, publicKey));
+ }
+
+
+ @Test
+ public void testSignAndVerifyFailure() throws GeneralSecurityException, IOException {
+ ED25519KeyAlgorithm testCase = new ED25519KeyAlgorithm();
+ byte[] message = "Signature Test 2".getBytes(StandardCharsets.UTF_8);
+ KeyPairGenerator factory = KeyPairGenerator.getInstance("EdDSA", new EdDSASecurityProvider());
+ KeyPair keyPair = factory.generateKeyPair();
+ EdDSAPrivateKey privateKey = (EdDSAPrivateKey) keyPair.getPrivate();
+ EdDSAPublicKey publicKey = (EdDSAPublicKey) keyPair.getPublic();
+ byte[] signature = testCase.generateSignature("Other Message".getBytes(StandardCharsets.UTF_8), privateKey, new SecureRandom());
+ assertFalse(testCase.verifySignature(message, signature, publicKey));
+ }
+
+
+ @Test
+ public void testParsePrivateKey() throws IOException {
+ PEMDecoder.decodeKeyPair(IOUtils.toCharArray(getClass().getResourceAsStream("ed25519-testkey-unprotected.txt")), null);
+ //Once we get decryption working: PEMDecoder.decodeKeyPair(IOUtils.toCharArray(getClass().getResourceAsStream("ed25519-testkey-protected.txt")), "password");
+ }
+}
diff --git a/test/com/trilead/ssh2/signature/ed25519-testkey-protected.txt b/test/com/trilead/ssh2/signature/ed25519-testkey-protected.txt
new file mode 100644
index 00000000..25cdbb5b
--- /dev/null
+++ b/test/com/trilead/ssh2/signature/ed25519-testkey-protected.txt
@@ -0,0 +1,9 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABDH
+od3uLgF991YsWnYQ5jdDAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIDkG
+sdrSLukFpIF3ddqYhLZ3Ktuz8cA7shqRoHeFE544AAAAoKwA3H2PTQPpiGCgCtzP
+T4rfyqd129XBS7UxUpWMHhSeZhU9US+8W5bNKw2T+PYD/xr76UtXIVmqochkYf2P
+e4vZRYi7A/ATtBn3Zq4LARfkJQDLPL3rvQgBt4rp3VMbS3cOnYf8QkS5nxPLHqWM
+3mVE4pZUZHPh7DryKC6GV1KrDVj3GL1mZ43WaxpdMoinox6iLVWTbnKXkp9sL3Vt
+B8g=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/com/trilead/ssh2/signature/ed25519-testkey-unprotected.txt b/test/com/trilead/ssh2/signature/ed25519-testkey-unprotected.txt
new file mode 100644
index 00000000..edd35d12
--- /dev/null
+++ b/test/com/trilead/ssh2/signature/ed25519-testkey-unprotected.txt
@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz
+c2gtZWQyNTUxOQAAACA5BrHa0i7pBaSBd3XamIS2dyrbs/HAO7IakaB3hROeOAAA
+AKCyAERusgBEbgAAAAtzc2gtZWQyNTUxOQAAACA5BrHa0i7pBaSBd3XamIS2dyrb
+s/HAO7IakaB3hROeOAAAAEABnrtATvieUtV8EtEnoqJzL/4LGGLqfbkpWvbZuhq7
+bzkGsdrSLukFpIF3ddqYhLZ3Ktuz8cA7shqRoHeFE544AAAAFGVkMjU1MTkta2V5
+LTIwMTcwMzMwAQIDBAUGBwgJ
+-----END OPENSSH PRIVATE KEY-----