Merge remote-tracking branch 'origin/refactor-git-trust' into refactor-git-trust

This commit is contained in:
Michael Jerger 2023-08-03 18:40:39 +02:00
commit 76549e997a
9 changed files with 127 additions and 105 deletions

View file

@ -1,9 +0,0 @@
package org.domaindrivenarchitecture.provs.desktop.application
import org.domaindrivenarchitecture.provs.desktop.domain.GitTrust
class InitService {
fun init():List<GitTrust> {
}
}

View file

@ -1,20 +0,0 @@
package org.domaindrivenarchitecture.provs.desktop.domain
class GitTrusts(val trusts: List<GitTrust>) {
constructor():this(listOf<GitTrust>(
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<String>) {
}

View file

@ -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<HostKey>)
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=",
)
)
)

View file

@ -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<KnownHost> = defaultKnownHosts) = task {
for (knownHost in knownHosts) {
with(knownHost) {
addKnownHost(hostName, hostKeys, verifyKeys = true)
}
}
}

View file

@ -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))
}

View file

@ -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<String>?) = taskWithResult {
if (isHostKnown(host)) {
return@taskWithResult ProvResult(true, out = "Host already known")
}
fun Prov.addKnownHost(host: String, keysToBeAdded: List<String>?, 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<String>? {
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<String>): List<String> {
return sshKeys.map { x -> cmd("echo \"$x\" | ssh-keygen -lf -").out ?: "" }
private fun Prov.getSshKeys(host: String, keytype: String? = null): List<String>? {
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(" ") }
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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)
}
}