From aa681138837e610e6411eb78217c0af15d9ca40a Mon Sep 17 00:00:00 2001 From: Michael Clarke Date: Sun, 2 Apr 2017 21:37:10 +0100 Subject: [PATCH] Add initial support for unencrypted ED25519 keys --- pom.xml | 16 ++ .../ssh2/crypto/CertificateDecoder.java | 4 + src/com/trilead/ssh2/crypto/PEMDecoder.java | 16 +- .../ssh2/signature/ED25519KeyAlgorithm.java | 195 ++++++++++++++++++ .../trilead/ssh2/signature/KeyAlgorithm.java | 13 +- .../ssh2/signature/KeyAlgorithmManager.java | 1 + .../signature/ED25519KeyAlgorithmTest.java | 79 +++++++ .../signature/ed25519-testkey-protected.txt | 9 + .../signature/ed25519-testkey-unprotected.txt | 8 + 9 files changed, 328 insertions(+), 13 deletions(-) create mode 100644 src/com/trilead/ssh2/signature/ED25519KeyAlgorithm.java create mode 100644 test/com/trilead/ssh2/signature/ED25519KeyAlgorithmTest.java create mode 100644 test/com/trilead/ssh2/signature/ed25519-testkey-protected.txt create mode 100644 test/com/trilead/ssh2/signature/ed25519-testkey-unprotected.txt 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-----