Support ED25519 and ECDSA keys in the PuTTY format (#660)

* Support ED25519 PuTTY keys.

Fix #659

* PuTTYKeyFile: Use net.schmizz.sshj.common.Buffer instead of own KeyReader.

A tiny refactoring made in order to allow usage of other utility methods which require Buffer.

* Support ECDSA PuTTY keys.

* Some code cleanup

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
This commit is contained in:
Vladimir Lagunov
2021-01-09 04:44:19 +07:00
committed by GitHub
parent 6d7dd741de
commit 9bc9262842
11 changed files with 227 additions and 52 deletions

View File

@@ -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;
@@ -101,17 +112,17 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
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 {
@@ -129,16 +140,13 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
}
}
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();
BigInteger p = publicKeyReader.readMPInt();
BigInteger q = publicKeyReader.readMPInt();
BigInteger g = publicKeyReader.readMPInt();
BigInteger y = publicKeyReader.readMPInt();
final KeyReader privateKeyReader = new KeyReader(privateKey);
// Private exponent from the private key
BigInteger x = privateKeyReader.readInt();
BigInteger x = privateKeyReader.readMPInt();
final KeyFactory factory;
try {
@@ -154,9 +162,42 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
} 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 {
@@ -297,40 +338,4 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
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());
}
}
}

View File

@@ -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;
@@ -246,6 +250,97 @@ public class PuTTYKeyFileTest {
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();

View File

@@ -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

View File

@@ -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-----

View File

@@ -0,0 +1 @@
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMi0QY0bIwaf20IgzigLd+lTuNXENA9FGQH9C/yV8ziaZK5gs9xqRXpq/QBOUzqYoP9M5tZA04kBjOncyxSfUQyBUt7+x31Nj+E/rxWf7O6YyqOa11fvRPLl+0Sgf3MGWA== root@sshj

View File

@@ -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

View File

@@ -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-----

View File

@@ -0,0 +1 @@
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADeKUPZeSGORK4SPwpwhdaOGMpI59BARkEVnKg9PhBXyhGIZPU1FVw3zo9rKarwzxaAA0VUQBmnt4OpOewonosSmgAz3cMlrURE5FbBJ4NMPLkYjEwNiCD4IplWEAwmZL+Jq+xJfIpR/WJtij1vlLzLyunO+aJGD91AaR+WglEEu7tzMA== root@sshj

View File

@@ -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

View File

@@ -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

View File

@@ -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