From a9627b4b22ae7c2094375a3b719a2577316dc5a9 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 25 Feb 2026 14:27:28 +0100 Subject: [PATCH 1/4] utils: use CertUtils.generateRandomKeyPair to create SSH keypair --- pom.xml | 6 --- utils/pom.xml | 4 -- .../com/cloud/utils/ssh/SSHKeysHelper.java | 54 ++++++++++++++----- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/pom.xml b/pom.xml index b4e2ec57f81b..7767d3500525 100644 --- a/pom.xml +++ b/pom.xml @@ -161,7 +161,6 @@ 5.5.0 2.12.5 2.2.1 - 0.1.55 20231013 1.2 2.7.0 @@ -335,11 +334,6 @@ java-ipv6 ${cs.java-ipv6.version} - - com.jcraft - jsch - ${cs.jsch.version} - com.rabbitmq amqp-client diff --git a/utils/pom.xml b/utils/pom.xml index ee6df9602b8f..92bf145de388 100755 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -78,10 +78,6 @@ org.bouncycastle bctls-jdk15on - - com.jcraft - jsch - org.jasypt jasypt diff --git a/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java b/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java index f25881ca09bd..98f02810c49a 100644 --- a/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java +++ b/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java @@ -20,15 +20,17 @@ package com.cloud.utils.ssh; import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.KeyPair; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.interfaces.RSAPublicKey; +import org.apache.cloudstack.utils.security.CertUtils; import org.apache.commons.codec.binary.Base64; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.KeyPair; - public class SSHKeysHelper { private KeyPair keyPair; @@ -45,8 +47,8 @@ private static String toHexString(byte[] b) { public SSHKeysHelper(Integer keyLength) { try { - keyPair = KeyPair.genKeyPair(new JSch(), KeyPair.RSA, keyLength); - } catch (JSchException e) { + keyPair = CertUtils.generateRandomKeyPair(keyLength); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { e.printStackTrace(); } } @@ -105,17 +107,43 @@ public static String getPublicKeyFromKeyMaterial(String keyMaterial) { } public String getPublicKey() { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - keyPair.writePublicKey(baos, ""); + try { + RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + writeString(buffer,"ssh-rsa"); + writeBigInt(buffer, rsaPublicKey.getPublicExponent()); + writeBigInt(buffer, rsaPublicKey.getModulus()); - return baos.toString(); + String base64 = Base64.encodeBase64String(buffer.toByteArray()); + + return "ssh-rsa " + base64; + } catch (Exception e) { + e.printStackTrace(); + } + return null; } - public String getPrivateKey() { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - keyPair.writePrivateKey(baos); + private static void writeString(ByteArrayOutputStream out, String str) throws Exception { + byte[] data = str.getBytes("UTF-8"); + out.write(ByteBuffer.allocate(4).putInt(data.length).array()); + out.write(data); + } + + private static void writeBigInt(ByteArrayOutputStream out, BigInteger value) throws Exception { + byte[] data = value.toByteArray(); + out.write(ByteBuffer.allocate(4).putInt(data.length).array()); + out.write(data); + } - return baos.toString(); + public String getPrivateKey() { + try { + return CertUtils.privateKeyToPem(keyPair.getPrivate()); + } catch (Exception e) { + e.printStackTrace(); + } + return null; } } From aa95bc0350a4f3e912f4194d3b418e0b757aaa17 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 27 Feb 2026 17:08:58 +0100 Subject: [PATCH 2/4] utils: print "RSA PRIVATE KEY" instead of "PRIVATE KEY" --- test/integration/smoke/test_network.py | 2 +- .../main/java/com/cloud/utils/ssh/SSHKeysHelper.java | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/test/integration/smoke/test_network.py b/test/integration/smoke/test_network.py index b3e7fd3e42f4..fc60207ed7e8 100644 --- a/test/integration/smoke/test_network.py +++ b/test/integration/smoke/test_network.py @@ -2349,7 +2349,7 @@ def _get_ip_address_output(self, ssh): return '\n'.join(res) @attr(tags=["advanced", "shared"], required_hardware="true") - def test_01_deployVMInSharedNetwork(self): + def test_01_deployVMInSharedNetworkWithConfigDrive(self): try: self.virtual_machine = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], networkids=[self.shared_network.id, self.isolated_network.id], diff --git a/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java b/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java index 98f02810c49a..570e025196f8 100644 --- a/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java +++ b/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java @@ -20,6 +20,7 @@ package com.cloud.utils.ssh; import java.io.ByteArrayOutputStream; +import java.io.StringWriter; import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.KeyPair; @@ -30,6 +31,8 @@ import org.apache.cloudstack.utils.security.CertUtils; import org.apache.commons.codec.binary.Base64; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; public class SSHKeysHelper { @@ -139,7 +142,12 @@ private static void writeBigInt(ByteArrayOutputStream out, BigInteger value) thr public String getPrivateKey() { try { - return CertUtils.privateKeyToPem(keyPair.getPrivate()); + final PemObject pemObject = new PemObject("RSA PRIVATE KEY", keyPair.getPrivate().getEncoded()); + final StringWriter sw = new StringWriter(); + try (final PemWriter pw = new PemWriter(sw)) { + pw.writeObject(pemObject); + } + return sw.toString(); } catch (Exception e) { e.printStackTrace(); } From 7be5a86784e9fa2c034623f8c6ec33ea5973a6b3 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 5 Mar 2026 12:06:22 +0100 Subject: [PATCH 3/4] SSHKeysHelper: add null checks and add unit tests --- .../com/cloud/utils/ssh/SSHKeysHelper.java | 9 ++- .../cloud/utils/ssh/SSHKeysHelperTest.java | 58 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java b/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java index 570e025196f8..b02789333323 100644 --- a/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java +++ b/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java @@ -23,6 +23,7 @@ import java.io.StringWriter; import java.math.BigInteger; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -110,6 +111,9 @@ public static String getPublicKeyFromKeyMaterial(String keyMaterial) { } public String getPublicKey() { + if (keyPair == null || keyPair.getPublic() == null) { + return null; + } try { RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); @@ -129,7 +133,7 @@ public String getPublicKey() { } private static void writeString(ByteArrayOutputStream out, String str) throws Exception { - byte[] data = str.getBytes("UTF-8"); + byte[] data = str.getBytes(StandardCharsets.UTF_8); out.write(ByteBuffer.allocate(4).putInt(data.length).array()); out.write(data); } @@ -141,6 +145,9 @@ private static void writeBigInt(ByteArrayOutputStream out, BigInteger value) thr } public String getPrivateKey() { + if (keyPair == null || keyPair.getPrivate() == null) { + return null; + } try { final PemObject pemObject = new PemObject("RSA PRIVATE KEY", keyPair.getPrivate().getEncoded()); final StringWriter sw = new StringWriter(); diff --git a/utils/src/test/java/com/cloud/utils/ssh/SSHKeysHelperTest.java b/utils/src/test/java/com/cloud/utils/ssh/SSHKeysHelperTest.java index a3daf9154b86..fef1fc4e713c 100644 --- a/utils/src/test/java/com/cloud/utils/ssh/SSHKeysHelperTest.java +++ b/utils/src/test/java/com/cloud/utils/ssh/SSHKeysHelperTest.java @@ -19,8 +19,14 @@ package com.cloud.utils.ssh; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + import org.junit.Test; public class SSHKeysHelperTest { @@ -70,4 +76,56 @@ public void dsaKeyTest() { assertTrue("fc:6e:ef:31:93:f8:92:2b:a9:03:c7:06:90:f5:ec:bb".equals(fingerprint)); } + + @Test + public void getPublicKeyFromKeyMaterialShouldHandleSupportedPrefixes() { + assertEquals("ecdsa-sha2-nistp256 AAAA", SSHKeysHelper.getPublicKeyFromKeyMaterial("ecdsa-sha2-nistp256 AAAA comment")); + assertEquals("ecdsa-sha2-nistp384 AAAA", SSHKeysHelper.getPublicKeyFromKeyMaterial("ecdsa-sha2-nistp384 AAAA comment")); + assertEquals("ecdsa-sha2-nistp521 AAAA", SSHKeysHelper.getPublicKeyFromKeyMaterial("ecdsa-sha2-nistp521 AAAA comment")); + assertEquals("ssh-ed25519 AAAA", SSHKeysHelper.getPublicKeyFromKeyMaterial("ssh-ed25519 AAAA comment")); + } + + @Test + public void getPublicKeyFromKeyMaterialShouldParseBase64EncodedMaterial() { + String keyMaterial = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKeyData comment"; + String encoded = Base64.getEncoder().encodeToString(keyMaterial.getBytes(StandardCharsets.UTF_8)); + + assertEquals("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKeyData", SSHKeysHelper.getPublicKeyFromKeyMaterial(encoded)); + } + + @Test + public void getPublicKeyFromKeyMaterialShouldReturnNullForInvalidFormats() { + assertNull(SSHKeysHelper.getPublicKeyFromKeyMaterial("not-a-valid-key")); + assertNull(SSHKeysHelper.getPublicKeyFromKeyMaterial("ssh-unknown AAAA")); + assertNull(SSHKeysHelper.getPublicKeyFromKeyMaterial("ssh-rsa")); + } + + @Test(expected = RuntimeException.class) + public void getPublicKeyFingerprintShouldThrowForInvalidPublicKey() { + SSHKeysHelper.getPublicKeyFingerprint("invalid-key-format"); + } + + @Test + public void generatedKeysShouldBeWellFormedAndFingerprintConsistent() { + SSHKeysHelper helper = new SSHKeysHelper(2048); + + String publicKey = helper.getPublicKey(); + String privateKey = helper.getPrivateKey(); + String fingerprint = helper.getPublicKeyFingerPrint(); + + assertNotNull(publicKey); + assertTrue(publicKey.startsWith("ssh-rsa ")); + + String[] keyParts = publicKey.split(" "); + assertEquals(2, keyParts.length); + + assertNotNull(privateKey); + assertTrue(privateKey.contains("BEGIN RSA PRIVATE KEY")); + assertTrue(privateKey.contains("END RSA PRIVATE KEY")); + + assertNotNull(fingerprint); + assertEquals(SSHKeysHelper.getPublicKeyFingerprint(publicKey), fingerprint); + + assertTrue("Legacy MD5 fingerprint should be colon-separated hex", fingerprint.matches("^([0-9a-f]{2}:){15}[0-9a-f]{2}$")); + } } From 738ec8696329bc5881e0536af1a7f67f7ef0fde2 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Thu, 5 Mar 2026 12:25:51 +0100 Subject: [PATCH 4/4] .github: update .pre-commit-config.yaml --- .pre-commit-config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1a7db702204..a871d9146bd6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -82,7 +82,8 @@ repos: ^services/console-proxy/rdpconsole/src/test/doc/rdp-key\.pem$| ^systemvm/agent/certs/localhost\.key$| ^systemvm/agent/certs/realhostip\.key$| - ^test/integration/smoke/test_ssl_offloading\.py$ + ^test/integration/smoke/test_ssl_offloading\.py$| + ^utils/src/test/java/com/cloud/utils/ssh/SSHKeysHelperTest\.java$ - id: end-of-file-fixer exclude: \.vhd$ - id: file-contents-sorter