Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ED25519 and ECDSA keys in the PuTTY format #660

Merged
merged 4 commits into from
Jan 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,20 @@
package net.schmizz.sshj.userauth.keyprovider;

import com.hierynomus.sshj.common.KeyAlgorithm;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import net.schmizz.sshj.common.Base64;
import net.schmizz.sshj.common.Buffer;
import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.common.SecurityUtils;
import net.schmizz.sshj.userauth.password.PasswordUtils;
import org.bouncycastle.asn1.nist.NISTNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.util.encoders.Hex;

import javax.crypto.Cipher;
Expand Down Expand Up @@ -101,17 +112,17 @@ public boolean isEncrypted() {

protected KeyPair readKeyPair() throws IOException {
this.parseKeyPair();
final Buffer.PlainBuffer publicKeyReader = new Buffer.PlainBuffer(publicKey);
final Buffer.PlainBuffer privateKeyReader = new Buffer.PlainBuffer(privateKey);
publicKeyReader.readBytes(); // The first part of the payload is a human-readable key format name.
if (KeyType.RSA.equals(this.getType())) {
final KeyReader publicKeyReader = new KeyReader(publicKey);
publicKeyReader.skip(); // skip this
// public key exponent
BigInteger e = publicKeyReader.readInt();
BigInteger e = publicKeyReader.readMPInt();
// modulus
BigInteger n = publicKeyReader.readInt();
BigInteger n = publicKeyReader.readMPInt();

final KeyReader privateKeyReader = new KeyReader(privateKey);
// private key exponent
BigInteger d = privateKeyReader.readInt();
BigInteger d = privateKeyReader.readMPInt();

final KeyFactory factory;
try {
Expand All @@ -129,16 +140,13 @@ protected KeyPair readKeyPair() throws IOException {
}
}
if (KeyType.DSA.equals(this.getType())) {
final KeyReader publicKeyReader = new KeyReader(publicKey);
publicKeyReader.skip(); // skip this
BigInteger p = publicKeyReader.readInt();
BigInteger q = publicKeyReader.readInt();
BigInteger g = publicKeyReader.readInt();
BigInteger y = publicKeyReader.readInt();

final KeyReader privateKeyReader = new KeyReader(privateKey);
BigInteger p = publicKeyReader.readMPInt();
BigInteger q = publicKeyReader.readMPInt();
BigInteger g = publicKeyReader.readMPInt();
BigInteger y = publicKeyReader.readMPInt();

// Private exponent from the private key
BigInteger x = privateKeyReader.readInt();
BigInteger x = privateKeyReader.readMPInt();

final KeyFactory factory;
try {
Expand All @@ -154,9 +162,42 @@ protected KeyPair readKeyPair() throws IOException {
} catch (InvalidKeySpecException e) {
throw new IOException(e.getMessage(), e);
}
} else {
throw new IOException(String.format("Unknown key type %s", this.getType()));
}
if (KeyType.ED25519.equals(this.getType())) {
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(publicKeyReader.readBytes(), ed25519);
EdDSAPrivateKeySpec privateSpec = new EdDSAPrivateKeySpec(privateKeyReader.readBytes(), ed25519);
return new KeyPair(new EdDSAPublicKey(publicSpec), new EdDSAPrivateKey(privateSpec));
}
final String ecdsaCurve;
switch (this.getType()) {
case ECDSA256:
ecdsaCurve = "P-256";
break;
case ECDSA384:
ecdsaCurve = "P-384";
break;
case ECDSA521:
ecdsaCurve = "P-521";
break;
default:
ecdsaCurve = null;
break;
}
if (ecdsaCurve != null) {
BigInteger s = new BigInteger(1, privateKeyReader.readBytes());
X9ECParameters ecParams = NISTNamedCurves.getByName(ecdsaCurve);
ECNamedCurveSpec ecCurveSpec =
new ECNamedCurveSpec(ecdsaCurve, ecParams.getCurve(), ecParams.getG(), ecParams.getN());
ECPrivateKeySpec pks = new ECPrivateKeySpec(s, ecCurveSpec);
try {
PrivateKey privateKey = SecurityUtils.getKeyFactory(KeyAlgorithm.ECDSA).generatePrivate(pks);
return new KeyPair(getType().readPubKeyFromBuffer(publicKeyReader), privateKey);
} catch (GeneralSecurityException e) {
throw new IOException(e.getMessage(), e);
}
}
throw new IOException(String.format("Unknown key type %s", this.getType()));
}

protected void parseKeyPair() throws IOException {
Expand Down Expand Up @@ -297,40 +338,4 @@ private byte[] decrypt(final byte[] key, final String passphrase) throws IOExcep
throw new IOException(e.getMessage(), e);
}
}

/**
* Parses the putty key bit vector, which is an encoded sequence
* of {@link java.math.BigInteger}s.
*/
private final static class KeyReader {
private final DataInput di;

public KeyReader(byte[] key) {
this.di = new DataInputStream(new ByteArrayInputStream(key));
}

/**
* Skips an integer without reading it.
*/
public void skip() throws IOException {
final int read = di.readInt();
if (read != di.skipBytes(read)) {
throw new IOException(String.format("Failed to skip %d bytes", read));
}
}

private byte[] read() throws IOException {
int len = di.readInt();
byte[] r = new byte[len];
di.readFully(r);
return r;
}

/**
* Reads the next integer.
*/
public BigInteger readInt() throws IOException {
return new BigInteger(read());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@
*/
package net.schmizz.sshj.keyprovider;

import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile;
import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile;
import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile;
import net.schmizz.sshj.userauth.password.PasswordFinder;
import net.schmizz.sshj.userauth.password.Resource;
import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;

Expand Down Expand Up @@ -246,6 +250,97 @@ public void test8192() throws Exception {
assertNotNull(key.getPublic());
}

@Test
public void testEd25519() throws Exception {
// Generated with
// puttygen src/test/resources/keytypes/test_ed25519 -O private \
// -o src/test/resources/keytypes/test_ed25519_puttygen.ppk
PuTTYKeyFile key = new PuTTYKeyFile();
key.init(new File("src/test/resources/keytypes/test_ed25519_puttygen.ppk"));
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());

OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile();
referenceKey.init(new File("src/test/resources/keytypes/test_ed25519"));
assertEquals(key.getPrivate(), referenceKey.getPrivate());
assertEquals(key.getPublic(), referenceKey.getPublic());
}

@Test
public void testEd25519Encrypted() throws Exception {
// Generated with
// puttygen src/test/resources/keytypes/test_ed25519 -O private \
// -o src/test/resources/keytypes/test_ed25519_puttygen_protected.ppk \
// --new-passphrase <(echo 123456)
PuTTYKeyFile key = new PuTTYKeyFile();
key.init(new File("src/test/resources/keytypes/test_ed25519_puttygen_protected.ppk"), new PasswordFinder() {
@Override
public char[] reqPassword(Resource<?> resource) {
return "123456".toCharArray();
}

@Override
public boolean shouldRetry(Resource<?> resource) {
return false;
}
});
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());

OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile();
referenceKey.init(new File("src/test/resources/keytypes/test_ed25519"));
assertEquals(key.getPrivate(), referenceKey.getPrivate());
assertEquals(key.getPublic(), referenceKey.getPublic());
}

@Test
public void testEcDsa256() throws Exception {
// Generated with
// puttygen src/test/resources/keytypes/test_ecdsa_nistp256 -O private \
// -o src/test/resources/keytypes/test_ecdsa_nistp256_puttygen.ppk
PuTTYKeyFile key = new PuTTYKeyFile();
key.init(new File("src/test/resources/keytypes/test_ecdsa_nistp256_puttygen.ppk"));
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());

PKCS8KeyFile referenceKey = new PKCS8KeyFile();
referenceKey.init(new File("src/test/resources/keytypes/test_ecdsa_nistp256"));
assertEquals(key.getPrivate(), referenceKey.getPrivate());
assertEquals(key.getPublic(), referenceKey.getPublic());
}

@Test
public void testEcDsa384() throws Exception {
// Generated with
// puttygen src/test/resources/keytypes/test_ecdsa_nistp384_2 -O private \
// -o src/test/resources/keytypes/test_ecdsa_nistp384_2_puttygen.ppk
PuTTYKeyFile key = new PuTTYKeyFile();
key.init(new File("src/test/resources/keytypes/test_ecdsa_nistp384_2_puttygen.ppk"));
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());

OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile();
referenceKey.init(new File("src/test/resources/keytypes/test_ecdsa_nistp384_2"));
assertEquals(key.getPrivate(), referenceKey.getPrivate());
assertEquals(key.getPublic(), referenceKey.getPublic());
}

@Test
public void testEcDsa521() throws Exception {
// Generated with
// puttygen src/test/resources/keytypes/test_ecdsa_nistp521_2 -O private \
// -o src/test/resources/keytypes/test_ecdsa_nistp521_2_puttygen.ppk
PuTTYKeyFile key = new PuTTYKeyFile();
key.init(new File("src/test/resources/keytypes/test_ecdsa_nistp521_2_puttygen.ppk"));
assertNotNull(key.getPrivate());
assertNotNull(key.getPublic());

OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile();
referenceKey.init(new File("src/test/resources/keytypes/test_ecdsa_nistp521_2"));
assertEquals(key.getPrivate(), referenceKey.getPrivate());
assertEquals(key.getPublic(), referenceKey.getPublic());
}

@Test
public void testCorrectPassphraseRsa() throws Exception {
PuTTYKeyFile key = new PuTTYKeyFile();
Expand Down
10 changes: 10 additions & 0 deletions src/test/resources/keytypes/test_ecdsa_nistp256_puttygen.ppk
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
PuTTY-User-Key-File-2: ecdsa-sha2-nistp256
Encryption: none
Comment: imported-openssh-key
Public-Lines: 3
AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOEQcvowiV3i
gdRO7rKPrZrao1hCQrnC4tgsxqSJdQCbABI+vHrdbJRfWZNuSk48aAtARJzJVmkn
/r63EPJgkh8=
Private-Lines: 1
AAAAIQCVDJbEpV6gmZgo5TeJFe4cz/qfabtH8CfK+JtapXufEg==
Private-MAC: 48f3a17cf5f65f4f225e7a21f007d8270d7c8c8f
10 changes: 10 additions & 0 deletions src/test/resources/keytypes/test_ecdsa_nistp384_2
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTItEGNGyMGn9tCIM4oC3fpU7jVxDQP
RRkB/Qv8lfM4mmSuYLPcakV6av0ATlM6mKD/TObWQNOJAYzp3MsUn1EMgVLe/sd9TY/hP6
8Vn+zumMqjmtdX70Ty5ftEoH9zBlgAAADYhfSye4X0snsAAAATZWNkc2Etc2hhMi1uaXN0
cDM4NAAAAAhuaXN0cDM4NAAAAGEEyLRBjRsjBp/bQiDOKAt36VO41cQ0D0UZAf0L/JXzOJ
pkrmCz3GpFemr9AE5TOpig/0zm1kDTiQGM6dzLFJ9RDIFS3v7HfU2P4T+vFZ/s7pjKo5rX
V+9E8uX7RKB/cwZYAAAAMGvH38HMnj6cELCBVQnAQYHlA/Vz1+RVZHj08cey/P3PALx7MR
pV135UZNZAtWQm+wAAAAlyb290QHNzaGoBAgMEBQYH
-----END OPENSSH PRIVATE KEY-----
1 change: 1 addition & 0 deletions src/test/resources/keytypes/test_ecdsa_nistp384_2.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMi0QY0bIwaf20IgzigLd+lTuNXENA9FGQH9C/yV8ziaZK5gs9xqRXpq/QBOUzqYoP9M5tZA04kBjOncyxSfUQyBUt7+x31Nj+E/rxWf7O6YyqOa11fvRPLl+0Sgf3MGWA== root@sshj
11 changes: 11 additions & 0 deletions src/test/resources/keytypes/test_ecdsa_nistp384_2_puttygen.ppk
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
PuTTY-User-Key-File-2: ecdsa-sha2-nistp384
Encryption: none
Comment: root@sshj
Public-Lines: 3
AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMi0QY0bIwaf
20IgzigLd+lTuNXENA9FGQH9C/yV8ziaZK5gs9xqRXpq/QBOUzqYoP9M5tZA04kB
jOncyxSfUQyBUt7+x31Nj+E/rxWf7O6YyqOa11fvRPLl+0Sgf3MGWA==
Private-Lines: 2
AAAAMGvH38HMnj6cELCBVQnAQYHlA/Vz1+RVZHj08cey/P3PALx7MRpV135UZNZA
tWQm+w==
Private-MAC: aa4d48441934e15491af0a30f75a02f4e324e652
12 changes: 12 additions & 0 deletions src/test/resources/keytypes/test_ecdsa_nistp521_2
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQA3ilD2XkhjkSuEj8KcIXWjhjKSOfQ
QEZBFZyoPT4QV8oRiGT1NRVcN86Paymq8M8WgANFVEAZp7eDqTnsKJ6LEpoAM93DJa1ERO
RWwSeDTDy5GIxMDYgg+CKZVhAMJmS/iavsSXyKUf1ibYo9b5S8y8rpzvmiRg/dQGkfloJR
BLu7czAAAAEI8uaocPLmqHAAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
AAAIUEAN4pQ9l5IY5ErhI/CnCF1o4Yykjn0EBGQRWcqD0+EFfKEYhk9TUVXDfOj2spqvDP
FoADRVRAGae3g6k57CieixKaADPdwyWtRETkVsEng0w8uRiMTA2IIPgimVYQDCZkv4mr7E
l8ilH9Ym2KPW+UvMvK6c75okYP3UBpH5aCUQS7u3MwAAAAQSlrwjeSrVTc6OyiA3OTfac4
+3nKcf/PRSjIhOLsGUIs2pVCxGYP8/ZfbVfkv7nHMn5Cc0fDZEs2cSWi2QhVKBSfAAAACX
Jvb3RAc3NoagEC
-----END OPENSSH PRIVATE KEY-----
1 change: 1 addition & 0 deletions src/test/resources/keytypes/test_ecdsa_nistp521_2.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADeKUPZeSGORK4SPwpwhdaOGMpI59BARkEVnKg9PhBXyhGIZPU1FVw3zo9rKarwzxaAA0VUQBmnt4OpOewonosSmgAz3cMlrURE5FbBJ4NMPLkYjEwNiCD4IplWEAwmZL+Jq+xJfIpR/WJtij1vlLzLyunO+aJGD91AaR+WglEEu7tzMA== root@sshj
12 changes: 12 additions & 0 deletions src/test/resources/keytypes/test_ecdsa_nistp521_2_puttygen.ppk
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
PuTTY-User-Key-File-2: ecdsa-sha2-nistp521
Encryption: none
Comment: root@sshj
Public-Lines: 4
AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADeKUPZeSGO
RK4SPwpwhdaOGMpI59BARkEVnKg9PhBXyhGIZPU1FVw3zo9rKarwzxaAA0VUQBmn
t4OpOewonosSmgAz3cMlrURE5FbBJ4NMPLkYjEwNiCD4IplWEAwmZL+Jq+xJfIpR
/WJtij1vlLzLyunO+aJGD91AaR+WglEEu7tzMA==
Private-Lines: 2
AAAAQSlrwjeSrVTc6OyiA3OTfac4+3nKcf/PRSjIhOLsGUIs2pVCxGYP8/ZfbVfk
v7nHMn5Cc0fDZEs2cSWi2QhVKBSf
Private-MAC: 052d1a2fe2c5837aec9dbe0bf10f2ccc376eda43
9 changes: 9 additions & 0 deletions src/test/resources/keytypes/test_ed25519_puttygen.ppk
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
PuTTY-User-Key-File-2: ssh-ed25519
Encryption: none
Comment: root@sshj
Public-Lines: 2
AAAAC3NzaC1lZDI1NTE5AAAAIDAdJiRkkBM8yC8seTEoAn2PfwbLKrkcahZ0xxPo
WICJ
Private-Lines: 1
AAAAIKaxyRDJxad8ZArpe1ClowY4NsCQxA50k0rpclKKkHt0
Private-MAC: 388f807649f181243015cad9650633ec28b25208
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
PuTTY-User-Key-File-2: ssh-ed25519
Encryption: aes256-cbc
Comment: root@sshj
Public-Lines: 2
AAAAC3NzaC1lZDI1NTE5AAAAIDAdJiRkkBM8yC8seTEoAn2PfwbLKrkcahZ0xxPo
WICJ
Private-Lines: 1
XFJyRzRt5NjuCVhDEyb50sI+gRn8FB65hh0U8uhGvP3VBl4haChinQasOTBYa4pj
Private-MAC: 80f50e1a7075567980742644460edffeb67ca829