Skip to content

Commit

Permalink
Add initial support for unencrypted ED25519 keys
Browse files Browse the repository at this point in the history
  • Loading branch information
mc1arke committed Apr 10, 2017
1 parent 8794946 commit aa68113
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 13 deletions.
16 changes: 16 additions & 0 deletions pom.xml
Expand Up @@ -49,11 +49,27 @@
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.i2p.crypto</groupId>
<artifactId>eddsa</artifactId>
<version>0.2.0-SNAPSHOT</version>
</dependency>
</dependencies>

<build>
<sourceDirectory>src</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<testResources>
<testResource>
<directory>test</directory>
<includes>
<include>**</include>
</includes>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</testResource>
</testResources>
<plugins>
<plugin>
<artifactId>maven-release-plugin</artifactId>
Expand Down
4 changes: 4 additions & 0 deletions src/com/trilead/ssh2/crypto/CertificateDecoder.java
Expand Up @@ -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;
}
16 changes: 6 additions & 10 deletions src/com/trilead/ssh2/crypto/PEMDecoder.java
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
}
Expand Down
195 changes: 195 additions & 0 deletions 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<EdDSAPublicKey, EdDSAPrivateKey> {

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


}

}
13 changes: 10 additions & 3 deletions src/com/trilead/ssh2/signature/KeyAlgorithm.java
Expand Up @@ -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;
Expand All @@ -17,28 +18,34 @@ public abstract class KeyAlgorithm<U extends PublicKey, R extends PrivateKey> {
private final String signatureAlgorithm;
private final String keyFormat;
private final Class<R> keyType;
private final Provider provider;

protected KeyAlgorithm(String signatureAlgorithm, String keyFormat, Class<R> keyType) {
this(signatureAlgorithm, keyFormat, keyType, null);
}

protected KeyAlgorithm(String signatureAlgorithm, String keyFormat, Class<R> 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);
Expand Down
1 change: 1 addition & 0 deletions src/com/trilead/ssh2/signature/KeyAlgorithmManager.java
Expand Up @@ -25,6 +25,7 @@ public static Collection<KeyAlgorithm<PublicKey, PrivateKey>> getSupportedAlgori

private static Collection<KeyAlgorithm<PublicKey, PrivateKey>> buildSupportAlgorithmsList() {
List<KeyAlgorithm<?, ?>> algorithms = new ArrayList<>();
algorithms.add(new ED25519KeyAlgorithm());

algorithms.add(new RSAKeyAlgorithm());
algorithms.add(new DSAKeyAlgorithm());
Expand Down

0 comments on commit aa68113

Please sign in to comment.