From 2fff923539e11ad35dd600b3f44b9ed5c31f0315 Mon Sep 17 00:00:00 2001 From: ansgarz Date: Sun, 27 Aug 2023 12:54:21 +0200 Subject: [PATCH] add port to KnownHost --- .../provs/desktop/domain/KnownHost.kt | 19 ++++++-- .../provs/framework/ubuntu/keys/base/Ssh.kt | 30 +++++++----- .../provs/desktop/domain/KnownHostTest.kt | 48 +++++++++++++++++-- 3 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHost.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHost.kt index 75b1e49..b164f4a 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHost.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHost.kt @@ -1,26 +1,35 @@ package org.domaindrivenarchitecture.provs.desktop.domain +typealias HostKey = String + /** - * A HostKey should contain space-separated: keytype, key and (optionally) a comment + * Represents a known host for ssh connections. + * + * @param hostName domain name or ip + * @param port (optional) to be specified if different from default port 22 + * @param hostKeys list of keys, where each should contain separated by space: 1. keytype, 2. key and 3. (optionally) a comment * * See: https://man7.org/linux/man-pages/man8/sshd.8.html#SSH_KNOWN_HOSTS_FILE_FORMAT */ -typealias HostKey = String - open class KnownHost( val hostName: String, + val port: Int? = null, val hostKeys: List ) { + constructor(hostName: String, hostKeys: List) : this(hostName, null, hostKeys) + companion object { val GITHUB = KnownHost( - "github.com", listOf( + "github.com", + listOf( "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl", "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=", ) ) val GITLAB = KnownHost( - "gitlab.com", listOf( + "gitlab.com", + listOf( "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9", "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=", diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/keys/base/Ssh.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/keys/base/Ssh.kt index 3cfe7e9..044e68d 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/keys/base/Ssh.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/keys/base/Ssh.kt @@ -19,18 +19,21 @@ fun Prov.configureSshKeys(sshKeys: SshKeyPair) = task { /** - * Checks if the specified hostname or Ip is in a known_hosts file - * - * @return whether if was found + * Checks if the specified host (domain name or IP) and (optional) port is contained in the known_hosts file */ -fun Prov.isKnownHost(hostOrIp: String): Boolean { - return cmdNoEval("ssh-keygen -F $hostOrIp").out?.isNotEmpty() ?: false +fun Prov.isKnownHost(hostOrIp: String, port: Int? = null): Boolean { + val hostWithPotentialPort = port?.let { hostInKnownHostsFileFormat(hostOrIp, port) } ?: hostOrIp + return cmdNoEval("ssh-keygen -F $hostWithPotentialPort").out?.isNotEmpty() ?: false +} + +fun hostInKnownHostsFileFormat(hostOrIp: String, port: Int? = null): String { + return port?.let { "[$hostOrIp]:$port" } ?: hostOrIp } /** * Adds ssh keys for specified host (which also can be an ip-address) to the ssh-file "known_hosts". - * If parameter verifyKeys is true the keys are checked against the live keys of the host and only added if valid. + * If parameter verifyKeys is true, the keys are checked against the live keys of the host and added only if valid. */ fun Prov.addKnownHost(knownHost: KnownHost, verifyKeys: Boolean = false) = task { val knownHostsFile = "~/.ssh/known_hosts" @@ -44,9 +47,10 @@ fun Prov.addKnownHost(knownHost: KnownHost, verifyKeys: Boolean = false) = task if (!verifyKeys) { addTextToFile("\n$hostName $key\n", File(knownHostsFile)) } else { - val validKeys = getSshKeys(hostName) + val validKeys = findSshKeys(hostName, port) if (validKeys?.contains(key) == true) { - addTextToFile("\n$hostName $key\n", File(knownHostsFile)) + val formattedHost = hostInKnownHostsFileFormat(hostName, port) + addTextToFile("\n$formattedHost $key\n", File(knownHostsFile)) } else { addResultToEval( ProvResult( @@ -62,10 +66,14 @@ fun Prov.addKnownHost(knownHost: KnownHost, verifyKeys: Boolean = false) = task /** - * Returns a list of valid ssh keys for the given host (host can also be an ip address), keys are returned as keytype and key BUT WITHOUT the host name + * Returns a list of valid ssh keys for the given host (host can also be an ip address), + * keys are returned (space-separated) as keytype and key, but WITHOUT the host name.* + * If no port is specified, the keys for the default port (22) are returned. + * If no keytype is specified, keys are returned for all keytypes. */ -private fun Prov.getSshKeys(host: String, keytype: String? = null): List? { +fun Prov.findSshKeys(host: String, port: Int? = null, keytype: String? = null): List? { + val portOption = port?.let { " -p $port " } ?: "" val keytypeOption = keytype?.let { " -t $keytype " } ?: "" - val output = cmd("ssh-keyscan $keytypeOption $host 2>/dev/null").out?.trim() + val output = cmd("ssh-keyscan $portOption $keytypeOption $host 2>/dev/null").out?.trim() return output?.split("\n")?.filter { x -> "" != x }?.map { x -> x.substringAfter(" ") } } diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostTest.kt index b4b785d..63435d0 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostTest.kt @@ -2,10 +2,11 @@ package org.domaindrivenarchitecture.provs.desktop.domain import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.deleteFile import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall +import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.addKnownHost +import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.isKnownHost import org.domaindrivenarchitecture.provs.test.defaultTestContainer import org.domaindrivenarchitecture.provs.test.tags.ContainerTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test @@ -29,10 +30,10 @@ class KnownHostTest { // Subclass of KnownHost for test knownHostSubclass_includes_additional_host - class KnownHostsSubclass(hostName: String, hostKeys: List): KnownHost(hostName, hostKeys) { + class KnownHostsSubclass(hostName: String, port: Int?, hostKeys: List): KnownHost(hostName, port, hostKeys) { companion object { - val ANOTHER_HOST = KnownHostsSubclass("anotherhost.com", listOf("key1")) + val ANOTHER_HOST = KnownHostsSubclass("anotherhost.com", 2222, listOf("key1")) fun values(): List { return values + ANOTHER_HOST @@ -49,5 +50,44 @@ class KnownHostTest { assertTrue(hosts.size > 1) assertEquals("key1", hosts.last().hostKeys[0]) } + + @ContainerTest + fun knownHost_with_port_verified_successfully() { + // given + val prov = defaultTestContainer() + prov.task { + aptInstall("ssh") + deleteFile("~/.ssh/known_hosts") + } + + // when + assertFalse(prov.isKnownHost(KnownHost.GITHUB.hostName)) + assertFalse(prov.isKnownHost(KnownHost.GITHUB.hostName, 22)) + val res = prov.addKnownHost(KnownHost(KnownHost.GITHUB.hostName, 22, KnownHost.GITHUB.hostKeys), verifyKeys = true) + + // then + assertTrue(res.success) + assertFalse(prov.isKnownHost(KnownHost.GITHUB.hostName)) + assertTrue(prov.isKnownHost(KnownHost.GITHUB.hostName, 22)) + } + + @ContainerTest + fun knownHost_with_port_verification_failing() { + // given + val prov = defaultTestContainer() + prov.task { + aptInstall("ssh") + deleteFile("~/.ssh/known_hosts") + } + + // when + assertFalse(prov.isKnownHost(KnownHost.GITHUB.hostName, 80)) + val res2 = prov.addKnownHost(KnownHost(KnownHost.GITHUB.hostName, 80, KnownHost.GITHUB.hostKeys), verifyKeys = true) + + // then + assertFalse(res2.success) + assertFalse(prov.isKnownHost(KnownHost.GITHUB.hostName)) + assertFalse(prov.isKnownHost(KnownHost.GITHUB.hostName, 80)) + } }