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 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/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/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..b02789333323 100644 --- a/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java +++ b/utils/src/main/java/com/cloud/utils/ssh/SSHKeysHelper.java @@ -20,14 +20,20 @@ package com.cloud.utils.ssh; import java.io.ByteArrayOutputStream; +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; +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; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; public class SSHKeysHelper { @@ -45,8 +51,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 +111,54 @@ public static String getPublicKeyFromKeyMaterial(String keyMaterial) { } public String getPublicKey() { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - keyPair.writePublicKey(baos, ""); + if (keyPair == null || keyPair.getPublic() == null) { + return null; + } + try { + RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - return baos.toString(); + writeString(buffer,"ssh-rsa"); + writeBigInt(buffer, rsaPublicKey.getPublicExponent()); + writeBigInt(buffer, rsaPublicKey.getModulus()); + + 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(StandardCharsets.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() { + 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(); + try (final PemWriter pw = new PemWriter(sw)) { + pw.writeObject(pemObject); + } + return sw.toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; } } 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}$")); + } }