Skip to content

Commit b577559

Browse files
nindanaotoclaude
andauthored
Fix key export for non-CRT RSA keys and add Ed25519 PEM support
- Handle RSAPrivateKey that doesn't implement RSAPrivateCrtKey (e.g., Conscrypt's OpenSSLRSAPrivateKey) by parsing PKCS#8 encoded form to extract CRT parameters - Add Ed25519 private key encoding to PEMEncoder in PKCS#8 format - Add tests for non-CRT RSA key export and Ed25519 PEM encoding Fixes connectbot/connectbot#1812 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent caf9f68 commit b577559

File tree

4 files changed

+351
-1
lines changed

4 files changed

+351
-1
lines changed

src/main/java/com/trilead/ssh2/crypto/OpenSSHKeyEncoder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.security.interfaces.ECPrivateKey;
3535
import java.security.interfaces.ECPublicKey;
3636
import java.security.interfaces.RSAPrivateCrtKey;
37+
import java.security.interfaces.RSAPrivateKey;
3738
import java.security.interfaces.RSAPublicKey;
3839
import java.security.spec.ECPoint;
3940
import java.security.spec.InvalidKeySpecException;
@@ -648,6 +649,14 @@ public static String exportOpenSSH(PrivateKey privateKey, PublicKey publicKey, S
648649
throws InvalidKeyException {
649650
if (privateKey instanceof RSAPrivateCrtKey && publicKey instanceof RSAPublicKey) {
650651
return exportOpenSSHRSA((RSAPrivateCrtKey) privateKey, (RSAPublicKey) publicKey, comment, passphrase);
652+
} else if (privateKey instanceof RSAPrivateKey && publicKey instanceof RSAPublicKey) {
653+
// Handle non-CRT RSA keys (e.g., from Conscrypt's OpenSSLRSAPrivateKey)
654+
try {
655+
RSAPrivateCrtKey crtKey = PEMEncoder.convertToRSAPrivateCrtKey((RSAPrivateKey) privateKey);
656+
return exportOpenSSHRSA(crtKey, (RSAPublicKey) publicKey, comment, passphrase);
657+
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
658+
throw new InvalidKeyException("Failed to convert RSA key to CRT format", e);
659+
}
651660
} else if (privateKey instanceof DSAPrivateKey && publicKey instanceof DSAPublicKey) {
652661
return exportOpenSSHDSA((DSAPrivateKey) privateKey, (DSAPublicKey) publicKey, comment, passphrase);
653662
} else if (privateKey instanceof ECPrivateKey && publicKey instanceof ECPublicKey) {

src/main/java/com/trilead/ssh2/crypto/PEMEncoder.java

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.trilead.ssh2.crypto.cipher.BlockCipher;
55
import com.trilead.ssh2.crypto.cipher.DES;
66
import com.trilead.ssh2.crypto.cipher.DESede;
7+
import com.trilead.ssh2.crypto.keys.Ed25519PrivateKey;
78
import com.trilead.ssh2.signature.ECDSASHA2Verify;
89

910
import java.io.IOException;
@@ -15,10 +16,14 @@
1516
import java.security.NoSuchAlgorithmException;
1617
import java.security.PrivateKey;
1718
import java.security.SecureRandom;
19+
import java.security.KeyFactory;
1820
import java.security.interfaces.DSAPrivateKey;
1921
import java.security.interfaces.ECPrivateKey;
2022
import java.security.interfaces.RSAPrivateCrtKey;
23+
import java.security.interfaces.RSAPrivateKey;
2124
import java.security.spec.ECFieldFp;
25+
import java.security.spec.InvalidKeySpecException;
26+
import java.security.spec.RSAPrivateCrtKeySpec;
2227
import java.security.spec.ECParameterSpec;
2328
import java.security.spec.ECPoint;
2429
import java.util.Locale;
@@ -203,7 +208,7 @@ public static String encodePrivateKey(PrivateKey privateKey, String password)
203208
/**
204209
* Encode private key to PEM format with auto-detected key type and specified algorithm.
205210
*
206-
* @param privateKey private key (RSA, DSA, or EC)
211+
* @param privateKey private key (RSA, DSA, EC, or Ed25519)
207212
* @param password password for encryption, or null for unencrypted
208213
* @param algorithm encryption algorithm
209214
* @return PEM-encoded private key
@@ -214,15 +219,100 @@ public static String encodePrivateKey(PrivateKey privateKey, String password, St
214219
throws IOException, InvalidKeyException {
215220
if (privateKey instanceof RSAPrivateCrtKey) {
216221
return encodeRSAPrivateKey((RSAPrivateCrtKey) privateKey, password, algorithm);
222+
} else if (privateKey instanceof RSAPrivateKey) {
223+
// Handle non-CRT RSA keys (e.g., from Conscrypt's OpenSSLRSAPrivateKey)
224+
try {
225+
RSAPrivateCrtKey crtKey = convertToRSAPrivateCrtKey((RSAPrivateKey) privateKey);
226+
return encodeRSAPrivateKey(crtKey, password, algorithm);
227+
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
228+
throw new InvalidKeyException("Failed to convert RSA key to CRT format", e);
229+
}
217230
} else if (privateKey instanceof DSAPrivateKey) {
218231
return encodeDSAPrivateKey((DSAPrivateKey) privateKey, password, algorithm);
219232
} else if (privateKey instanceof ECPrivateKey) {
220233
return encodeECPrivateKey((ECPrivateKey) privateKey, password, algorithm);
234+
} else if (privateKey instanceof Ed25519PrivateKey) {
235+
return encodeEd25519PrivateKey((Ed25519PrivateKey) privateKey, password, algorithm);
221236
} else {
222237
throw new InvalidKeyException("Unsupported key type: " + privateKey.getClass().getName());
223238
}
224239
}
225240

241+
/**
242+
* Encode Ed25519 private key to PEM format (PKCS#8).
243+
*
244+
* @param privateKey Ed25519 private key
245+
* @param password password for encryption, or null for unencrypted
246+
* @return PEM-encoded private key
247+
* @throws IOException if encoding fails
248+
*/
249+
public static String encodeEd25519PrivateKey(Ed25519PrivateKey privateKey, String password) throws IOException {
250+
return encodeEd25519PrivateKey(privateKey, password, DEFAULT_ENCRYPTION);
251+
}
252+
253+
/**
254+
* Encode Ed25519 private key to PEM format (PKCS#8) with specified encryption algorithm.
255+
*
256+
* @param privateKey Ed25519 private key
257+
* @param password password for encryption, or null for unencrypted
258+
* @param algorithm encryption algorithm
259+
* @return PEM-encoded private key
260+
* @throws IOException if encoding fails
261+
*/
262+
public static String encodeEd25519PrivateKey(Ed25519PrivateKey privateKey, String password, String algorithm)
263+
throws IOException {
264+
// Ed25519 uses PKCS#8 format (BEGIN PRIVATE KEY)
265+
byte[] encoded = privateKey.getEncoded();
266+
return formatPEM("PRIVATE KEY", encoded, password, algorithm);
267+
}
268+
269+
/**
270+
* Converts an RSAPrivateKey to RSAPrivateCrtKey by parsing the PKCS#8 encoded form.
271+
* This is needed for keys from providers like Conscrypt that don't implement RSAPrivateCrtKey.
272+
*
273+
* @param privateKey The RSA private key to convert
274+
* @return The RSAPrivateCrtKey with CRT parameters
275+
* @throws InvalidKeySpecException if the key cannot be parsed
276+
* @throws NoSuchAlgorithmException if RSA algorithm is not available
277+
*/
278+
static RSAPrivateCrtKey convertToRSAPrivateCrtKey(RSAPrivateKey privateKey)
279+
throws InvalidKeySpecException, NoSuchAlgorithmException {
280+
byte[] encoded = privateKey.getEncoded();
281+
try {
282+
SimpleDERReader reader = new SimpleDERReader(encoded);
283+
reader.resetInput(reader.readSequenceAsByteArray());
284+
if (!reader.readInt().equals(BigInteger.ZERO)) {
285+
throw new InvalidKeySpecException("PKCS#8 is not version 0");
286+
}
287+
288+
reader.readSequenceAsByteArray(); // OID sequence
289+
reader.resetInput(reader.readOctetString()); // RSA key bytes
290+
reader.resetInput(reader.readSequenceAsByteArray()); // RSA key sequence
291+
292+
if (!reader.readInt().equals(BigInteger.ZERO)) {
293+
throw new InvalidKeySpecException("RSA key is not version 0");
294+
}
295+
296+
BigInteger modulus = reader.readInt();
297+
BigInteger publicExponent = reader.readInt();
298+
BigInteger privateExponent = reader.readInt();
299+
BigInteger primeP = reader.readInt();
300+
BigInteger primeQ = reader.readInt();
301+
BigInteger primeExponentP = reader.readInt();
302+
BigInteger primeExponentQ = reader.readInt();
303+
BigInteger crtCoefficient = reader.readInt();
304+
305+
RSAPrivateCrtKeySpec spec = new RSAPrivateCrtKeySpec(
306+
modulus, publicExponent, privateExponent,
307+
primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient);
308+
309+
KeyFactory kf = KeyFactory.getInstance("RSA");
310+
return (RSAPrivateCrtKey) kf.generatePrivate(spec);
311+
} catch (IOException e) {
312+
throw new InvalidKeySpecException("Could not parse RSA key", e);
313+
}
314+
}
315+
226316
private static String formatPEM(String type, byte[] data, String password, String algorithm) throws IOException {
227317
byte[] encodedData = data;
228318

src/test/java/com/trilead/ssh2/crypto/OpenSSHKeyEncoderTest.java

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import static org.junit.jupiter.api.Assertions.assertTrue;
66

77
import java.io.IOException;
8+
import java.math.BigInteger;
89
import java.security.KeyPair;
910
import java.security.KeyPairGenerator;
1011
import java.security.interfaces.DSAPrivateKey;
1112
import java.security.interfaces.DSAPublicKey;
1213
import java.security.interfaces.ECPrivateKey;
1314
import java.security.interfaces.ECPublicKey;
1415
import java.security.interfaces.RSAPrivateCrtKey;
16+
import java.security.interfaces.RSAPrivateKey;
1517
import java.security.interfaces.RSAPublicKey;
1618
import java.security.spec.ECGenParameterSpec;
1719

@@ -309,4 +311,101 @@ public void testDecodeGoldenEd25519AndReencode() throws Exception {
309311
assertEquals(kp.getPublic(), decoded.getPublic());
310312
assertEquals(kp.getPrivate(), decoded.getPrivate());
311313
}
314+
315+
/**
316+
* Tests that non-CRT RSA keys (like Conscrypt's OpenSSLRSAPrivateKey) can be exported.
317+
* This simulates the scenario where an RSAPrivateKey does not implement RSAPrivateCrtKey.
318+
*/
319+
@Test
320+
public void testExportOpenSSHWithNonCrtRSAKey() throws Exception {
321+
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
322+
kpg.initialize(2048);
323+
KeyPair original = kpg.generateKeyPair();
324+
325+
// Wrap the RSA private key to simulate a non-CRT key (like OpenSSLRSAPrivateKey)
326+
RSAPrivateKey nonCrtKey = new NonCrtRSAPrivateKeyWrapper((RSAPrivateCrtKey) original.getPrivate());
327+
328+
// Verify our wrapper is not an instance of RSAPrivateCrtKey
329+
assertTrue(nonCrtKey instanceof RSAPrivateKey);
330+
assertTrue(!(nonCrtKey instanceof RSAPrivateCrtKey));
331+
332+
// Export using the generic method which should handle non-CRT keys
333+
String exported = OpenSSHKeyEncoder.exportOpenSSH(
334+
nonCrtKey,
335+
original.getPublic(),
336+
"test-non-crt");
337+
338+
assertNotNull(exported);
339+
assertTrue(exported.contains("-----BEGIN OPENSSH PRIVATE KEY-----"));
340+
assertTrue(exported.contains("-----END OPENSSH PRIVATE KEY-----"));
341+
342+
// Verify round-trip
343+
KeyPair decoded = PEMDecoder.decode(exported.toCharArray(), null);
344+
345+
assertEquals(original.getPublic(), decoded.getPublic());
346+
assertEquals(original.getPrivate(), decoded.getPrivate());
347+
}
348+
349+
/**
350+
* Tests that non-CRT RSA keys can be exported with encryption.
351+
*/
352+
@Test
353+
public void testExportOpenSSHWithNonCrtRSAKeyEncrypted() throws Exception {
354+
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
355+
kpg.initialize(2048);
356+
KeyPair original = kpg.generateKeyPair();
357+
358+
RSAPrivateKey nonCrtKey = new NonCrtRSAPrivateKeyWrapper((RSAPrivateCrtKey) original.getPrivate());
359+
360+
String exported = OpenSSHKeyEncoder.exportOpenSSH(
361+
nonCrtKey,
362+
original.getPublic(),
363+
"test-non-crt",
364+
"testpassword");
365+
366+
assertNotNull(exported);
367+
assertTrue(exported.contains("-----BEGIN OPENSSH PRIVATE KEY-----"));
368+
369+
KeyPair decoded = PEMDecoder.decode(exported.toCharArray(), "testpassword");
370+
371+
assertEquals(original.getPublic(), decoded.getPublic());
372+
assertEquals(original.getPrivate(), decoded.getPrivate());
373+
}
374+
375+
/**
376+
* A wrapper that implements RSAPrivateKey but NOT RSAPrivateCrtKey.
377+
* This simulates keys from providers like Conscrypt's OpenSSLRSAPrivateKey.
378+
*/
379+
private static class NonCrtRSAPrivateKeyWrapper implements RSAPrivateKey {
380+
private final RSAPrivateCrtKey delegate;
381+
382+
NonCrtRSAPrivateKeyWrapper(RSAPrivateCrtKey delegate) {
383+
this.delegate = delegate;
384+
}
385+
386+
@Override
387+
public BigInteger getPrivateExponent() {
388+
return delegate.getPrivateExponent();
389+
}
390+
391+
@Override
392+
public String getAlgorithm() {
393+
return delegate.getAlgorithm();
394+
}
395+
396+
@Override
397+
public String getFormat() {
398+
return delegate.getFormat();
399+
}
400+
401+
@Override
402+
public byte[] getEncoded() {
403+
return delegate.getEncoded();
404+
}
405+
406+
@Override
407+
public BigInteger getModulus() {
408+
return delegate.getModulus();
409+
}
410+
}
312411
}

0 commit comments

Comments
 (0)