Skip to content

Commit 9bc9262

Browse files
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>
1 parent 6d7dd74 commit 9bc9262

11 files changed

+228
-53
lines changed

src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java

Lines changed: 58 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,20 @@
1616
package net.schmizz.sshj.userauth.keyprovider;
1717

1818
import com.hierynomus.sshj.common.KeyAlgorithm;
19+
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
20+
import net.i2p.crypto.eddsa.EdDSAPublicKey;
21+
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
22+
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
23+
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
24+
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
1925
import net.schmizz.sshj.common.Base64;
26+
import net.schmizz.sshj.common.Buffer;
2027
import net.schmizz.sshj.common.KeyType;
28+
import net.schmizz.sshj.common.SecurityUtils;
2129
import net.schmizz.sshj.userauth.password.PasswordUtils;
30+
import org.bouncycastle.asn1.nist.NISTNamedCurves;
31+
import org.bouncycastle.asn1.x9.X9ECParameters;
32+
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
2233
import org.bouncycastle.util.encoders.Hex;
2334

2435
import javax.crypto.Cipher;
@@ -101,17 +112,17 @@ public boolean isEncrypted() {
101112

102113
protected KeyPair readKeyPair() throws IOException {
103114
this.parseKeyPair();
115+
final Buffer.PlainBuffer publicKeyReader = new Buffer.PlainBuffer(publicKey);
116+
final Buffer.PlainBuffer privateKeyReader = new Buffer.PlainBuffer(privateKey);
117+
publicKeyReader.readBytes(); // The first part of the payload is a human-readable key format name.
104118
if (KeyType.RSA.equals(this.getType())) {
105-
final KeyReader publicKeyReader = new KeyReader(publicKey);
106-
publicKeyReader.skip(); // skip this
107119
// public key exponent
108-
BigInteger e = publicKeyReader.readInt();
120+
BigInteger e = publicKeyReader.readMPInt();
109121
// modulus
110-
BigInteger n = publicKeyReader.readInt();
122+
BigInteger n = publicKeyReader.readMPInt();
111123

112-
final KeyReader privateKeyReader = new KeyReader(privateKey);
113124
// private key exponent
114-
BigInteger d = privateKeyReader.readInt();
125+
BigInteger d = privateKeyReader.readMPInt();
115126

116127
final KeyFactory factory;
117128
try {
@@ -129,16 +140,13 @@ protected KeyPair readKeyPair() throws IOException {
129140
}
130141
}
131142
if (KeyType.DSA.equals(this.getType())) {
132-
final KeyReader publicKeyReader = new KeyReader(publicKey);
133-
publicKeyReader.skip(); // skip this
134-
BigInteger p = publicKeyReader.readInt();
135-
BigInteger q = publicKeyReader.readInt();
136-
BigInteger g = publicKeyReader.readInt();
137-
BigInteger y = publicKeyReader.readInt();
138-
139-
final KeyReader privateKeyReader = new KeyReader(privateKey);
143+
BigInteger p = publicKeyReader.readMPInt();
144+
BigInteger q = publicKeyReader.readMPInt();
145+
BigInteger g = publicKeyReader.readMPInt();
146+
BigInteger y = publicKeyReader.readMPInt();
147+
140148
// Private exponent from the private key
141-
BigInteger x = privateKeyReader.readInt();
149+
BigInteger x = privateKeyReader.readMPInt();
142150

143151
final KeyFactory factory;
144152
try {
@@ -154,9 +162,42 @@ protected KeyPair readKeyPair() throws IOException {
154162
} catch (InvalidKeySpecException e) {
155163
throw new IOException(e.getMessage(), e);
156164
}
157-
} else {
158-
throw new IOException(String.format("Unknown key type %s", this.getType()));
159165
}
166+
if (KeyType.ED25519.equals(this.getType())) {
167+
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
168+
EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(publicKeyReader.readBytes(), ed25519);
169+
EdDSAPrivateKeySpec privateSpec = new EdDSAPrivateKeySpec(privateKeyReader.readBytes(), ed25519);
170+
return new KeyPair(new EdDSAPublicKey(publicSpec), new EdDSAPrivateKey(privateSpec));
171+
}
172+
final String ecdsaCurve;
173+
switch (this.getType()) {
174+
case ECDSA256:
175+
ecdsaCurve = "P-256";
176+
break;
177+
case ECDSA384:
178+
ecdsaCurve = "P-384";
179+
break;
180+
case ECDSA521:
181+
ecdsaCurve = "P-521";
182+
break;
183+
default:
184+
ecdsaCurve = null;
185+
break;
186+
}
187+
if (ecdsaCurve != null) {
188+
BigInteger s = new BigInteger(1, privateKeyReader.readBytes());
189+
X9ECParameters ecParams = NISTNamedCurves.getByName(ecdsaCurve);
190+
ECNamedCurveSpec ecCurveSpec =
191+
new ECNamedCurveSpec(ecdsaCurve, ecParams.getCurve(), ecParams.getG(), ecParams.getN());
192+
ECPrivateKeySpec pks = new ECPrivateKeySpec(s, ecCurveSpec);
193+
try {
194+
PrivateKey privateKey = SecurityUtils.getKeyFactory(KeyAlgorithm.ECDSA).generatePrivate(pks);
195+
return new KeyPair(getType().readPubKeyFromBuffer(publicKeyReader), privateKey);
196+
} catch (GeneralSecurityException e) {
197+
throw new IOException(e.getMessage(), e);
198+
}
199+
}
200+
throw new IOException(String.format("Unknown key type %s", this.getType()));
160201
}
161202

162203
protected void parseKeyPair() throws IOException {
@@ -297,40 +338,4 @@ private byte[] decrypt(final byte[] key, final String passphrase) throws IOExcep
297338
throw new IOException(e.getMessage(), e);
298339
}
299340
}
300-
301-
/**
302-
* Parses the putty key bit vector, which is an encoded sequence
303-
* of {@link java.math.BigInteger}s.
304-
*/
305-
private final static class KeyReader {
306-
private final DataInput di;
307-
308-
public KeyReader(byte[] key) {
309-
this.di = new DataInputStream(new ByteArrayInputStream(key));
310-
}
311-
312-
/**
313-
* Skips an integer without reading it.
314-
*/
315-
public void skip() throws IOException {
316-
final int read = di.readInt();
317-
if (read != di.skipBytes(read)) {
318-
throw new IOException(String.format("Failed to skip %d bytes", read));
319-
}
320-
}
321-
322-
private byte[] read() throws IOException {
323-
int len = di.readInt();
324-
byte[] r = new byte[len];
325-
di.readFully(r);
326-
return r;
327-
}
328-
329-
/**
330-
* Reads the next integer.
331-
*/
332-
public BigInteger readInt() throws IOException {
333-
return new BigInteger(read());
334-
}
335-
}
336341
}

src/test/java/net/schmizz/sshj/keyprovider/PuTTYKeyFileTest.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@
1515
*/
1616
package net.schmizz.sshj.keyprovider;
1717

18+
import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile;
19+
import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile;
1820
import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile;
1921
import net.schmizz.sshj.userauth.password.PasswordFinder;
2022
import net.schmizz.sshj.userauth.password.Resource;
2123
import org.junit.Test;
2224

25+
import java.io.File;
2326
import java.io.IOException;
2427
import java.io.StringReader;
2528

29+
import static org.junit.Assert.assertEquals;
2630
import static org.junit.Assert.assertNotNull;
2731
import static org.junit.Assert.assertNull;
2832

@@ -246,6 +250,97 @@ public void test8192() throws Exception {
246250
assertNotNull(key.getPublic());
247251
}
248252

253+
@Test
254+
public void testEd25519() throws Exception {
255+
// Generated with
256+
// puttygen src/test/resources/keytypes/test_ed25519 -O private \
257+
// -o src/test/resources/keytypes/test_ed25519_puttygen.ppk
258+
PuTTYKeyFile key = new PuTTYKeyFile();
259+
key.init(new File("src/test/resources/keytypes/test_ed25519_puttygen.ppk"));
260+
assertNotNull(key.getPrivate());
261+
assertNotNull(key.getPublic());
262+
263+
OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile();
264+
referenceKey.init(new File("src/test/resources/keytypes/test_ed25519"));
265+
assertEquals(key.getPrivate(), referenceKey.getPrivate());
266+
assertEquals(key.getPublic(), referenceKey.getPublic());
267+
}
268+
269+
@Test
270+
public void testEd25519Encrypted() throws Exception {
271+
// Generated with
272+
// puttygen src/test/resources/keytypes/test_ed25519 -O private \
273+
// -o src/test/resources/keytypes/test_ed25519_puttygen_protected.ppk \
274+
// --new-passphrase <(echo 123456)
275+
PuTTYKeyFile key = new PuTTYKeyFile();
276+
key.init(new File("src/test/resources/keytypes/test_ed25519_puttygen_protected.ppk"), new PasswordFinder() {
277+
@Override
278+
public char[] reqPassword(Resource<?> resource) {
279+
return "123456".toCharArray();
280+
}
281+
282+
@Override
283+
public boolean shouldRetry(Resource<?> resource) {
284+
return false;
285+
}
286+
});
287+
assertNotNull(key.getPrivate());
288+
assertNotNull(key.getPublic());
289+
290+
OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile();
291+
referenceKey.init(new File("src/test/resources/keytypes/test_ed25519"));
292+
assertEquals(key.getPrivate(), referenceKey.getPrivate());
293+
assertEquals(key.getPublic(), referenceKey.getPublic());
294+
}
295+
296+
@Test
297+
public void testEcDsa256() throws Exception {
298+
// Generated with
299+
// puttygen src/test/resources/keytypes/test_ecdsa_nistp256 -O private \
300+
// -o src/test/resources/keytypes/test_ecdsa_nistp256_puttygen.ppk
301+
PuTTYKeyFile key = new PuTTYKeyFile();
302+
key.init(new File("src/test/resources/keytypes/test_ecdsa_nistp256_puttygen.ppk"));
303+
assertNotNull(key.getPrivate());
304+
assertNotNull(key.getPublic());
305+
306+
PKCS8KeyFile referenceKey = new PKCS8KeyFile();
307+
referenceKey.init(new File("src/test/resources/keytypes/test_ecdsa_nistp256"));
308+
assertEquals(key.getPrivate(), referenceKey.getPrivate());
309+
assertEquals(key.getPublic(), referenceKey.getPublic());
310+
}
311+
312+
@Test
313+
public void testEcDsa384() throws Exception {
314+
// Generated with
315+
// puttygen src/test/resources/keytypes/test_ecdsa_nistp384_2 -O private \
316+
// -o src/test/resources/keytypes/test_ecdsa_nistp384_2_puttygen.ppk
317+
PuTTYKeyFile key = new PuTTYKeyFile();
318+
key.init(new File("src/test/resources/keytypes/test_ecdsa_nistp384_2_puttygen.ppk"));
319+
assertNotNull(key.getPrivate());
320+
assertNotNull(key.getPublic());
321+
322+
OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile();
323+
referenceKey.init(new File("src/test/resources/keytypes/test_ecdsa_nistp384_2"));
324+
assertEquals(key.getPrivate(), referenceKey.getPrivate());
325+
assertEquals(key.getPublic(), referenceKey.getPublic());
326+
}
327+
328+
@Test
329+
public void testEcDsa521() throws Exception {
330+
// Generated with
331+
// puttygen src/test/resources/keytypes/test_ecdsa_nistp521_2 -O private \
332+
// -o src/test/resources/keytypes/test_ecdsa_nistp521_2_puttygen.ppk
333+
PuTTYKeyFile key = new PuTTYKeyFile();
334+
key.init(new File("src/test/resources/keytypes/test_ecdsa_nistp521_2_puttygen.ppk"));
335+
assertNotNull(key.getPrivate());
336+
assertNotNull(key.getPublic());
337+
338+
OpenSSHKeyV1KeyFile referenceKey = new OpenSSHKeyV1KeyFile();
339+
referenceKey.init(new File("src/test/resources/keytypes/test_ecdsa_nistp521_2"));
340+
assertEquals(key.getPrivate(), referenceKey.getPrivate());
341+
assertEquals(key.getPublic(), referenceKey.getPublic());
342+
}
343+
249344
@Test
250345
public void testCorrectPassphraseRsa() throws Exception {
251346
PuTTYKeyFile key = new PuTTYKeyFile();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
PuTTY-User-Key-File-2: ecdsa-sha2-nistp256
2+
Encryption: none
3+
Comment: imported-openssh-key
4+
Public-Lines: 3
5+
AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOEQcvowiV3i
6+
gdRO7rKPrZrao1hCQrnC4tgsxqSJdQCbABI+vHrdbJRfWZNuSk48aAtARJzJVmkn
7+
/r63EPJgkh8=
8+
Private-Lines: 1
9+
AAAAIQCVDJbEpV6gmZgo5TeJFe4cz/qfabtH8CfK+JtapXufEg==
10+
Private-MAC: 48f3a17cf5f65f4f225e7a21f007d8270d7c8c8f
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
3+
1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTItEGNGyMGn9tCIM4oC3fpU7jVxDQP
4+
RRkB/Qv8lfM4mmSuYLPcakV6av0ATlM6mKD/TObWQNOJAYzp3MsUn1EMgVLe/sd9TY/hP6
5+
8Vn+zumMqjmtdX70Ty5ftEoH9zBlgAAADYhfSye4X0snsAAAATZWNkc2Etc2hhMi1uaXN0
6+
cDM4NAAAAAhuaXN0cDM4NAAAAGEEyLRBjRsjBp/bQiDOKAt36VO41cQ0D0UZAf0L/JXzOJ
7+
pkrmCz3GpFemr9AE5TOpig/0zm1kDTiQGM6dzLFJ9RDIFS3v7HfU2P4T+vFZ/s7pjKo5rX
8+
V+9E8uX7RKB/cwZYAAAAMGvH38HMnj6cELCBVQnAQYHlA/Vz1+RVZHj08cey/P3PALx7MR
9+
pV135UZNZAtWQm+wAAAAlyb290QHNzaGoBAgMEBQYH
10+
-----END OPENSSH PRIVATE KEY-----
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMi0QY0bIwaf20IgzigLd+lTuNXENA9FGQH9C/yV8ziaZK5gs9xqRXpq/QBOUzqYoP9M5tZA04kBjOncyxSfUQyBUt7+x31Nj+E/rxWf7O6YyqOa11fvRPLl+0Sgf3MGWA== root@sshj
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
PuTTY-User-Key-File-2: ecdsa-sha2-nistp384
2+
Encryption: none
3+
Comment: root@sshj
4+
Public-Lines: 3
5+
AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMi0QY0bIwaf
6+
20IgzigLd+lTuNXENA9FGQH9C/yV8ziaZK5gs9xqRXpq/QBOUzqYoP9M5tZA04kB
7+
jOncyxSfUQyBUt7+x31Nj+E/rxWf7O6YyqOa11fvRPLl+0Sgf3MGWA==
8+
Private-Lines: 2
9+
AAAAMGvH38HMnj6cELCBVQnAQYHlA/Vz1+RVZHj08cey/P3PALx7MRpV135UZNZA
10+
tWQm+w==
11+
Private-MAC: aa4d48441934e15491af0a30f75a02f4e324e652
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
3+
1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQA3ilD2XkhjkSuEj8KcIXWjhjKSOfQ
4+
QEZBFZyoPT4QV8oRiGT1NRVcN86Paymq8M8WgANFVEAZp7eDqTnsKJ6LEpoAM93DJa1ERO
5+
RWwSeDTDy5GIxMDYgg+CKZVhAMJmS/iavsSXyKUf1ibYo9b5S8y8rpzvmiRg/dQGkfloJR
6+
BLu7czAAAAEI8uaocPLmqHAAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
7+
AAAIUEAN4pQ9l5IY5ErhI/CnCF1o4Yykjn0EBGQRWcqD0+EFfKEYhk9TUVXDfOj2spqvDP
8+
FoADRVRAGae3g6k57CieixKaADPdwyWtRETkVsEng0w8uRiMTA2IIPgimVYQDCZkv4mr7E
9+
l8ilH9Ym2KPW+UvMvK6c75okYP3UBpH5aCUQS7u3MwAAAAQSlrwjeSrVTc6OyiA3OTfac4
10+
+3nKcf/PRSjIhOLsGUIs2pVCxGYP8/ZfbVfkv7nHMn5Cc0fDZEs2cSWi2QhVKBSfAAAACX
11+
Jvb3RAc3NoagEC
12+
-----END OPENSSH PRIVATE KEY-----
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADeKUPZeSGORK4SPwpwhdaOGMpI59BARkEVnKg9PhBXyhGIZPU1FVw3zo9rKarwzxaAA0VUQBmnt4OpOewonosSmgAz3cMlrURE5FbBJ4NMPLkYjEwNiCD4IplWEAwmZL+Jq+xJfIpR/WJtij1vlLzLyunO+aJGD91AaR+WglEEu7tzMA== root@sshj
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
PuTTY-User-Key-File-2: ecdsa-sha2-nistp521
2+
Encryption: none
3+
Comment: root@sshj
4+
Public-Lines: 4
5+
AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADeKUPZeSGO
6+
RK4SPwpwhdaOGMpI59BARkEVnKg9PhBXyhGIZPU1FVw3zo9rKarwzxaAA0VUQBmn
7+
t4OpOewonosSmgAz3cMlrURE5FbBJ4NMPLkYjEwNiCD4IplWEAwmZL+Jq+xJfIpR
8+
/WJtij1vlLzLyunO+aJGD91AaR+WglEEu7tzMA==
9+
Private-Lines: 2
10+
AAAAQSlrwjeSrVTc6OyiA3OTfac4+3nKcf/PRSjIhOLsGUIs2pVCxGYP8/ZfbVfk
11+
v7nHMn5Cc0fDZEs2cSWi2QhVKBSf
12+
Private-MAC: 052d1a2fe2c5837aec9dbe0bf10f2ccc376eda43
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
PuTTY-User-Key-File-2: ssh-ed25519
2+
Encryption: none
3+
Comment: root@sshj
4+
Public-Lines: 2
5+
AAAAC3NzaC1lZDI1NTE5AAAAIDAdJiRkkBM8yC8seTEoAn2PfwbLKrkcahZ0xxPo
6+
WICJ
7+
Private-Lines: 1
8+
AAAAIKaxyRDJxad8ZArpe1ClowY4NsCQxA50k0rpclKKkHt0
9+
Private-MAC: 388f807649f181243015cad9650633ec28b25208
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
PuTTY-User-Key-File-2: ssh-ed25519
2+
Encryption: aes256-cbc
3+
Comment: root@sshj
4+
Public-Lines: 2
5+
AAAAC3NzaC1lZDI1NTE5AAAAIDAdJiRkkBM8yC8seTEoAn2PfwbLKrkcahZ0xxPo
6+
WICJ
7+
Private-Lines: 1
8+
XFJyRzRt5NjuCVhDEyb50sI+gRn8FB65hh0U8uhGvP3VBl4haChinQasOTBYa4pj
9+
Private-MAC: 80f50e1a7075567980742644460edffeb67ca829

0 commit comments

Comments
 (0)