From 79b0c811c2765b967965ceb8dbb2b65e8b7bdc97 Mon Sep 17 00:00:00 2001 From: ansgarz Date: Wed, 2 Aug 2023 20:43:17 +0200 Subject: [PATCH] refactorings - add addKnownHosts --- .../provs/desktop/application/InitService.kt | 9 --- .../provs/desktop/domain/GitTrust.kt | 20 ------ .../provs/desktop/domain/KnownHostConfig.kt | 31 +++++++++ .../provs/desktop/domain/KnownHostService.kt | 13 ++++ .../provs/framework/ubuntu/git/base/Git.kt | 25 -------- .../provs/framework/ubuntu/keys/base/Ssh.kt | 63 ++++++------------- .../provs/desktop/domain/KnownHostTest.kt | 29 +++++++++ .../framework/ubuntu/git/base/GitKtTest.kt | 7 +-- .../framework/ubuntu/keys/base/SshKtTest.kt | 35 ++++++++++- 9 files changed, 127 insertions(+), 105 deletions(-) delete mode 100644 src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/InitService.kt delete mode 100644 src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/GitTrust.kt create mode 100644 src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostConfig.kt create mode 100644 src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostService.kt create mode 100644 src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostTest.kt diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/InitService.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/InitService.kt deleted file mode 100644 index 8996dea..0000000 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/InitService.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.domaindrivenarchitecture.provs.desktop.application - -import org.domaindrivenarchitecture.provs.desktop.domain.GitTrust - -class InitService { - fun init():List { - - } -} \ No newline at end of file diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/GitTrust.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/GitTrust.kt deleted file mode 100644 index e8dda21..0000000 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/GitTrust.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.domaindrivenarchitecture.provs.desktop.domain - -class GitTrusts(val trusts: List) { - constructor():this(listOf( - GitTrust("github.com", setOf( - "SHA256:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s github.com", // (RSA) - "SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM github.com", // (ECDSA) - "SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU github.com" // (Ed25519) - )), - GitTrust("gitlab.com", setOf( - "gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf", - "gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9", - "gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=", - )) - )) {} -} - -class GitTrust(hostName: String, fingerprints: Set) { - -} \ No newline at end of file diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostConfig.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostConfig.kt new file mode 100644 index 0000000..07f8768 --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostConfig.kt @@ -0,0 +1,31 @@ +package org.domaindrivenarchitecture.provs.desktop.domain + +/** + * A HostKey should contain space-separated: keytype, key and (optionally) a comment + * + * See: https://man7.org/linux/man-pages/man8/sshd.8.html#SSH_KNOWN_HOSTS_FILE_FORMAT + */ +typealias HostKey = String + +data class KnownHost(val hostName: String, val hostKeys: List) + + +val defaultKnownHosts = listOf( + // github - https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints + KnownHost( + "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=", + ) + ), + // gitlab.com - https://docs.gitlab.com/ee/user/gitlab_com/ + KnownHost( + "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/desktop/domain/KnownHostService.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostService.kt new file mode 100644 index 0000000..72ccaac --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostService.kt @@ -0,0 +1,13 @@ +package org.domaindrivenarchitecture.provs.desktop.domain + +import org.domaindrivenarchitecture.provs.framework.core.Prov +import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.addKnownHost + + +fun Prov.addKnownHosts(knownHosts: List = defaultKnownHosts) = task { + for (knownHost in knownHosts) { + with(knownHost) { + addKnownHost(hostName, hostKeys, verifyKeys = true) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/git/base/Git.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/git/base/Git.kt index ddc04f7..d1bbe23 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/git/base/Git.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/git/base/Git.kt @@ -3,9 +3,6 @@ package org.domaindrivenarchitecture.provs.framework.ubuntu.git.base import org.domaindrivenarchitecture.provs.framework.core.Prov import org.domaindrivenarchitecture.provs.framework.core.ProvResult import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.* -import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.KNOWN_HOSTS_FILE -import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.trustHost -import java.io.File /** @@ -41,25 +38,3 @@ fun Prov.gitClone( } } - -fun Prov.trustGithub() = task { - // current fingerprints from https://docs.github.com/en/github/authenticating-to-github/githubs-ssh-key-fingerprints - val fingerprints = setOf( - "SHA256:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s github.com", // (RSA) - "SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM github.com", // (ECDSA) - "SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU github.com" // (Ed25519) - ) - trustHost("github.com", fingerprints) -} - - -fun Prov.trustGitlab() = task { - // entries for known_hosts from https://docs.gitlab.com/ee/user/gitlab_com/ - val gitlabFingerprints = """ - gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf - gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9 - gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY= - """.trimIndent() - addTextToFile("\n" + gitlabFingerprints + "\n", File(KNOWN_HOSTS_FILE)) -} - 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 4eb39ea..cee5133 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 @@ -2,12 +2,9 @@ package org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base import org.domaindrivenarchitecture.provs.framework.core.Prov import org.domaindrivenarchitecture.provs.framework.core.ProvResult -import org.domaindrivenarchitecture.provs.framework.core.echoCommandForText -import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkFile -import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDir -import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFile -import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createSecretFile +import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.* import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.SshKeyPair +import java.io.File const val KNOWN_HOSTS_FILE = "~/.ssh/known_hosts" @@ -34,61 +31,39 @@ fun Prov.isHostKnown(hostOrIp: String) : Boolean { /** * Adds ssh keys for specified host (which also can be an ip-address) to ssh-file "known_hosts" - * Either add the specified rsaFingerprints or - if null - add automatically retrieved keys. + * Either add the specified keys or - if null - add keys automatically retrieved. * Note: adding keys automatically is vulnerable to a man-in-the-middle attack, thus considered insecure and not recommended. */ -fun Prov.trustHost(host: String, fingerprintsOfKeysToBeAdded: Set?) = taskWithResult { - if (isHostKnown(host)) { - return@taskWithResult ProvResult(true, out = "Host already known") - } +fun Prov.addKnownHost(host: String, keysToBeAdded: List?, verifyKeys: Boolean = false) = task { if (!checkFile(KNOWN_HOSTS_FILE)) { createDir(".ssh") createFile(KNOWN_HOSTS_FILE, null) } - if (fingerprintsOfKeysToBeAdded == null) { + if (keysToBeAdded == null) { // auto add keys cmd("ssh-keyscan $host >> $KNOWN_HOSTS_FILE") } else { - // logic based on https://serverfault.com/questions/447028/non-interactive-git-clone-ssh-fingerprint-prompt - val actualKeys = findSshKeys(host) - if (actualKeys == null || actualKeys.size == 0) { - return@taskWithResult ProvResult(false, out = "No valid keys found for host: $host") - } - val actualFingerprints = getFingerprintsForKeys(actualKeys) - for (fingerprintToBeAdded in fingerprintsOfKeysToBeAdded) { - var indexOfKeyFound = -1 - - // search for fingerprint in actual fingerprints - for ((i, actualFingerprint) in actualFingerprints.withIndex()) { - if (actualFingerprint.contains(fingerprintToBeAdded)) { - indexOfKeyFound = i - break + for (key in keysToBeAdded) { + if (!verifyKeys) { + addTextToFile("\n$host $key\n", File(KNOWN_HOSTS_FILE)) + } else { + val validKeys = getSshKeys(host) + if (validKeys?.contains(key) == true) { + addTextToFile("\n$host $key\n", File(KNOWN_HOSTS_FILE)) + } else { + addResultToEval(ProvResult(false, err = "The following key of host [$host] could not be verified successfully: " + key)) } } - if (indexOfKeyFound == -1) { - return@taskWithResult ProvResult( - false, - err = "Fingerprint ($fingerprintToBeAdded) could not be found in actual fingerprints: $actualFingerprints" - ) - } - cmd(echoCommandForText(actualKeys.get(indexOfKeyFound) + "\n") + " >> $KNOWN_HOSTS_FILE") } - ProvResult(true) } } /** - * Returns a list of valid ssh keys for the given host (host can also be an ip address) + * 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 */ -private fun Prov.findSshKeys(host: String): List? { - return cmd("ssh-keyscan $host 2>/dev/null").out?.split("\n")?.filter { x -> "" != x } -} - - -/** - * Returns a list of fingerprints of the given sshKeys; the returning list has same size and order as the specified list of sshKeys - */ -private fun Prov.getFingerprintsForKeys(sshKeys: List): List { - return sshKeys.map { x -> cmd("echo \"$x\" | ssh-keygen -lf -").out ?: "" } +private fun Prov.getSshKeys(host: String, keytype: String? = null): List? { + val keytypeOption = keytype?.let { " -t $keytype " } ?: "" + val output = cmd("ssh-keyscan $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 new file mode 100644 index 0000000..b357033 --- /dev/null +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/KnownHostTest.kt @@ -0,0 +1,29 @@ +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.KNOWN_HOSTS_FILE +import org.domaindrivenarchitecture.provs.test.defaultTestContainer +import org.domaindrivenarchitecture.provs.test.tags.ContainerTest +import org.junit.jupiter.api.Assertions.assertTrue + + +class KnownHostTest { + + @ContainerTest + fun defaultKnownHosts() { + // given + val prov = defaultTestContainer() + prov.task { + aptInstall("ssh") + deleteFile(KNOWN_HOSTS_FILE) + } + + // when + val res = prov.addKnownHosts() + + // then + assertTrue(res.success) + } +} + diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/git/base/GitKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/git/base/GitKtTest.kt index 873d603..51f6a14 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/git/base/GitKtTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/git/base/GitKtTest.kt @@ -1,5 +1,6 @@ package org.domaindrivenarchitecture.provs.framework.ubuntu.git.base +import org.domaindrivenarchitecture.provs.desktop.domain.addKnownHosts import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkDir import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.isHostKnown @@ -18,12 +19,10 @@ internal class GitKtTest { a.aptInstall("openssh-client") // when - val res = a.trustGithub() - val res2 = a.trustGitlab() + val res = a.addKnownHosts() // then assertTrue(res.success) - assertTrue(res2.success) assertTrue(a.isHostKnown("github.com"), "github.com does not seem to be a known host") assertTrue(a.isHostKnown("gitlab.com"), "gitlab.com does not seem to be a known host") @@ -37,7 +36,7 @@ internal class GitKtTest { prov.aptInstall("git") // when - prov.trustGithub() + prov.addKnownHosts() val res1 = prov.gitClone("https://gitlab.com/domaindrivenarchitecture/not a valid basename.git", "~/") val res2 = prov.gitClone(repo) val res3 = prov.gitClone(repo, pullIfExisting = false) diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/keys/base/SshKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/keys/base/SshKtTest.kt index 8d5e999..5c317b8 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/keys/base/SshKtTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/keys/base/SshKtTest.kt @@ -1,15 +1,16 @@ package org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base import org.domaindrivenarchitecture.provs.framework.core.Secret +import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.deleteFile +import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileContainsText import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileContent +import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.* 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.* internal class SshKtTest { - @ContainerTest fun configureSshKeys_for_ssh_type_rsa() { // given @@ -45,4 +46,32 @@ internal class SshKtTest { val privateSshKeyFileContent = prov.fileContent("~/.ssh/id_ed25519") assertEquals(privateED25519SnakeOilKey() + "\n", privateSshKeyFileContent) } + + @ContainerTest + fun addKnownHost() { + // given + val prov = defaultTestContainer() + prov.task { + aptInstall("ssh") + deleteFile(KNOWN_HOSTS_FILE) + } + + // when + val res = prov.addKnownHost("github.com", listOf("dummyProtocol dummyKey", "dummyProtocol2 dummyKey2", )) + val res2 = prov.addKnownHost("github.com", listOf("dummyProtocol dummyKey", "dummyProtocol2 dummyKey2", )) + val res3 = prov.addKnownHost("github.com", listOf("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl", ), verifyKeys = true) + val res4 = prov.addKnownHost("github.com", listOf("ssh-ed25519 AAAAC3Nzalwrongkey!!!", ), verifyKeys = true) + + // then + assertTrue(res.success) + assertTrue(prov.fileContainsText(KNOWN_HOSTS_FILE, "github.com dummyProtocol dummyKey")) + assertTrue(prov.fileContainsText(KNOWN_HOSTS_FILE, "github.com dummyProtocol2 dummyKey2")) + + assertTrue(res2.success) + val keyCount = prov.cmd("grep -o -i dummyKey2 $KNOWN_HOSTS_FILE | wc -l").out?.trim() + assertEquals("1", keyCount) + + assertTrue(res3.success) + assertFalse(res4.success) + } } \ No newline at end of file