diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/WotCLI.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/WotCLI.kt index 23de6cb7..b085fe30 100644 --- a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/WotCLI.kt +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/WotCLI.kt @@ -9,15 +9,14 @@ import org.pgpainless.certificate_store.PGPainlessCertD import org.pgpainless.util.DateUtil import org.pgpainless.wot.KeyRingCertificateStore import org.pgpainless.wot.WebOfTrust -import org.pgpainless.wot.api.Binding +import org.pgpainless.wot.cli.format.Formatter import org.pgpainless.wot.api.WoTAPI +import org.pgpainless.wot.cli.format.HumanReadableFormatter import org.pgpainless.wot.cli.subcommands.* import org.pgpainless.wot.network.Fingerprint import org.pgpainless.wot.network.ReferenceTime import org.pgpainless.wot.network.Root import org.pgpainless.wot.network.Roots -import org.pgpainless.wot.query.Path -import pgp.cert_d.PGPCertificateDirectory import pgp.cert_d.PGPCertificateStoreAdapter import pgp.cert_d.SpecialNames import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory @@ -160,6 +159,8 @@ class WotCLI: Callable { return PGPCertificateStoreAdapter(certD) } + val formatter: Formatter = HumanReadableFormatter() + fun readGpgOwnertrust(): List = Runtime.getRuntime() .exec("/usr/bin/gpg --export-ownertrust") .inputStream diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/format/Formatter.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/format/Formatter.kt new file mode 100644 index 00000000..3776c0b9 --- /dev/null +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/format/Formatter.kt @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.wot.cli.format + +import org.pgpainless.wot.api.* + +interface Formatter { + + /** + * Format a binding. + * @param binding binding to format + * @param amountMin minimum trust amount to accept the binding + * @param amountReference reference value to compare the amount against to calculate percentage + */ + fun format(binding: Binding, amountMin: Int = 120, amountReference: Int = 120): String + + fun format(authenticateResult: AuthenticateAPI.Result): String { + return buildString { + append(format(authenticateResult.binding, authenticateResult.targetAmount)) + if (!authenticateResult.acceptable) { + appendLine() + append("Could not authenticate any paths.") + } + } + } + + fun format(identifyResult: IdentifyAPI.Result): String { + return buildString { + identifyResult.bindings.forEach { + appendLine(format(it, identifyResult.targetAmount)) + } + if (!identifyResult.acceptable) { + appendLine("Could not authenticate any paths.") + } + } + } + + fun format(listResult: ListAPI.Result): String { + return buildString { + listResult.bindings.forEach { + appendLine(format(it, listResult.targetAmount)) + } + } + } + + fun format(lookupResult: LookupAPI.Result): String { + return buildString { + lookupResult.bindings.forEach { + appendLine(format(it, lookupResult.targetAmount)) + } + if (!lookupResult.acceptable) { + appendLine("Could not authenticate any paths.") + } + } + } +} \ No newline at end of file diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/format/HumanReadableFormatter.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/format/HumanReadableFormatter.kt new file mode 100644 index 00000000..eee6e8da --- /dev/null +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/format/HumanReadableFormatter.kt @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.wot.cli.format + +import org.pgpainless.wot.api.Binding +import java.text.SimpleDateFormat + +class HumanReadableFormatter: Formatter { + + private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd") + + /** + * Format a single binding + */ + override fun format(binding: Binding, amountMin: Int, amountReference: Int): String { + val percentage = binding.percentage(amountReference) + val authLevel = when(binding.paths.amount) { + in 0..39 -> "not authenticated" + in 40..119 -> "partially authenticated" + in 120 .. 239 -> "fully authenticated" + else -> {if (percentage < 0) "not authenticated" else "doubly authenticated"} + } + val checkmark = if(binding.paths.amount >= amountMin) "[✓] " else "[ ] " + val pathList = binding.paths.paths + val singlePath = pathList.size == 1 + val indent = " ".repeat(if (singlePath) 2 else 4) + + return buildString { + // [✓] 7F9116FEA90A5983936C7CFAA027DB2F3E1E118A Paul Schaub : fully authenticated (100%) + append(checkmark); appendLine("${binding.fingerprint} ${binding.userId}: $authLevel (${percentage}%)") + for ((pIndex, path) in pathList.withIndex()) { + if (!singlePath) { + appendLine(" Path #${pIndex + 1} of ${pathList.size}, trust amount ${path.amount}:") + } + val originUserId = if (path.root.userIds.isEmpty()) + "" + else if (path.root.fingerprint == path.target.fingerprint) + " \"${path.root.userIds.keys.first()}\"" + else + " (\"${path.root.userIds.keys.first()}\")" + append(indent); appendLine("◯ ${path.root.fingerprint}$originUserId") + for ((eIndex, edge) in path.certifications.withIndex()) { + val targetUserId = if (edge.userId == null) "" else " \"${edge.userId}\"" + append(indent); appendLine("│ certified the following binding on ${dateFormat.format(edge.creationTime)}") + append(indent); append(if (eIndex != path.certifications.lastIndex) "├ " else "└ ") + appendLine("${edge.target.fingerprint}$targetUserId") + } + if (pIndex != pathList.lastIndex) { + appendLine() + } + } + } + } +} \ No newline at end of file diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmd.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmd.kt index 0aba2056..b4e6fc1a 100644 --- a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmd.kt +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmd.kt @@ -45,8 +45,6 @@ class AuthenticateCmd: Callable { @CommandLine.Option(names = ["--email"], description = ["Consider all user-IDs that contain the given email address."]) var email = false - - /** * Execute the command. * @return exit code @@ -54,20 +52,8 @@ class AuthenticateCmd: Callable { override fun call(): Int { val result = parent.api.authenticate(AuthenticateAPI.Arguments( Fingerprint(fingerprint), userId, email)) - formatResult(result) - if (result.percentage < 100) { - return -1 - } - return 0 - } - /** - * Format the [AuthenticateAPI.Result] as a [String] which can be printed to standard out. - */ - internal fun formatResult(result: AuthenticateAPI.Result) { - if (result.percentage < 100) { - println("No paths found.") - } - println(result.binding.toConsoleOut(result.targetAmount, WotCLI.dateFormat)) + println(parent.formatter.format(result)) + return if (result.acceptable) 0 else 1 } } \ No newline at end of file diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/IdentifyCmd.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/IdentifyCmd.kt index 07292c66..473539a7 100644 --- a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/IdentifyCmd.kt +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/IdentifyCmd.kt @@ -28,26 +28,9 @@ class IdentifyCmd: Callable { * @return exit code */ override fun call(): Int { - val api = parent.api - val result = api.identify(IdentifyAPI.Arguments(Fingerprint(fingerprint))) - println(formatResult(result, api.trustAmount, WotCLI.dateFormat)) - return exitCode(result) + val result = parent.api.identify(IdentifyAPI.Arguments(Fingerprint(fingerprint))) + + print(parent.formatter.format(result)) + return if (result.acceptable) 0 else 1 } - - fun formatResult(result: IdentifyAPI.Result, targetAmount: Int, dateFormat: SimpleDateFormat): String { - if (result.bindings.isEmpty()) { - return "No paths found." - } - - return buildString { - result.bindings.forEach { - appendLine(it.toConsoleOut(targetAmount, dateFormat)) - } - } - } - - fun exitCode(result: IdentifyAPI.Result): Int { - return if(result.bindings.isEmpty()) -1 else 0 - } - } \ No newline at end of file diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/ListCmd.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/ListCmd.kt index 6069c54d..732c90c3 100644 --- a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/ListCmd.kt +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/ListCmd.kt @@ -21,13 +21,9 @@ class ListCmd: Callable { * @return exit code */ override fun call(): Int { - val api = parent.api - val result = api.list() - println(buildString { - result.bindings.forEach { - appendLine(it.toConsoleOut(api.trustAmount, WotCLI.dateFormat)) - } - }) + val result = parent.api.list() + + println(parent.formatter.format(result)) return 0 } } \ No newline at end of file diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/LookupCmd.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/LookupCmd.kt index 3ea3ec2a..8ec8a0b1 100644 --- a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/LookupCmd.kt +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/LookupCmd.kt @@ -27,11 +27,9 @@ class LookupCmd: Callable { * @return exit code */ override fun call(): Int { - val api = parent.api - val result = api.lookup(LookupAPI.Arguments(userId, email)) - result.bindings.forEach { - println(it.toConsoleOut(api.trustAmount, WotCLI.dateFormat)) - } - return if (result.bindings.isEmpty()) -1 else 0 + val result = parent.api.lookup(LookupAPI.Arguments(userId, email)) + + print(parent.formatter.format(result)) + return if (result.acceptable) 0 else 1 } } \ No newline at end of file diff --git a/pgpainless-wot-cli/src/test/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmdTest.kt b/pgpainless-wot-cli/src/test/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmdTest.kt index 6d75423a..4d60dc97 100644 --- a/pgpainless-wot-cli/src/test/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmdTest.kt +++ b/pgpainless-wot-cli/src/test/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmdTest.kt @@ -1,7 +1,13 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.wot.cli.subcommands import org.junit.jupiter.api.Test import org.pgpainless.wot.api.AuthenticateAPI +import org.pgpainless.wot.api.Binding +import org.pgpainless.wot.cli.format.HumanReadableFormatter import org.pgpainless.wot.network.* import org.pgpainless.wot.query.Path import org.pgpainless.wot.query.Paths @@ -42,19 +48,18 @@ class AuthenticateCmdTest { Depth.limited(0), RegexSet.wildcard()) paths.add(Path(neal, mutableListOf(edgeComponent), Depth.auto(0)), 120) - val testResult = AuthenticateAPI.Result( + val testResult = AuthenticateAPI.Result(Binding( Fingerprint("CBCD8F030588653EEDD7E2659B7DD433F254904A"), "Justus Winter ", - 120, - paths) + paths), + 120, ) - val formatted = cmd.formatResult(testResult) + val formatted = HumanReadableFormatter().format(testResult) assertEquals(buildString { - append("[✓] CBCD8F030588653EEDD7E2659B7DD433F254904A Justus Winter : fully authenticated (100%)\n") - append(" Path #1 of 1, trust amount 120:\n") - append(" ◯ F7173B3C7C685CD9ECC4191B74E445BA0E15C957 (\"Neal H. Walfield (Code Signing Key) \")\n") - append(" │ certified the following binding on 2022-02-04\n") - append(" └ CBCD8F030588653EEDD7E2659B7DD433F254904A \"Justus Winter \"\n") + appendLine("[✓] CBCD8F030588653EEDD7E2659B7DD433F254904A Justus Winter : fully authenticated (100%)") + appendLine(" ◯ F7173B3C7C685CD9ECC4191B74E445BA0E15C957 (\"Neal H. Walfield (Code Signing Key) \")") + appendLine(" │ certified the following binding on 2022-02-04") + appendLine(" └ CBCD8F030588653EEDD7E2659B7DD433F254904A \"Justus Winter \"") }, formatted) } } \ No newline at end of file diff --git a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/WebOfTrust.kt b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/WebOfTrust.kt index abb284d8..fd2d7356 100644 --- a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/WebOfTrust.kt +++ b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/WebOfTrust.kt @@ -115,7 +115,7 @@ class WebOfTrust(private val certificateStore: PGPCertificateStore) { val expirationDate: Date? = try { cert.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER) } catch (e: NoSuchElementException) { - LOGGER.warn("Could not deduce expiration time of ${cert.fingerprint}. " + + LOGGER.debug("Could not deduce expiration time of ${cert.fingerprint}. " + "Possibly hard revoked cert or illegal algorithms? Skip certificate."); // Some keys are malformed and have no KeyFlags // TODO: We also end up here for expired keys unfortunately @@ -207,7 +207,7 @@ class WebOfTrust(private val certificateStore: PGPCertificateStore) { return // we're done } catch (e: SignatureValidationException) { val targetFingerprint = OpenPgpFingerprint.of(targetPrimaryKey) - LOGGER.warn("Cannot verify signature by $issuerFingerprint" + + LOGGER.debug("Cannot verify signature by $issuerFingerprint" + " on cert of $targetFingerprint", e) } } @@ -250,7 +250,7 @@ class WebOfTrust(private val certificateStore: PGPCertificateStore) { networkBuilder.addEdge(fromCertification(issuer, target, userId, certification)) return // we're done } catch (e: SignatureValidationException) { - LOGGER.warn("Cannot verify signature for '$userId' by $issuerFingerprint" + + LOGGER.debug("Cannot verify signature for '$userId' by $issuerFingerprint" + " on cert of ${target.fingerprint}", e) } } diff --git a/wot-dijkstra/build.gradle b/wot-dijkstra/build.gradle index f7e219f9..e06eda0a 100644 --- a/wot-dijkstra/build.gradle +++ b/wot-dijkstra/build.gradle @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub +// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 diff --git a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/AuthenticateAPI.kt b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/AuthenticateAPI.kt index c2d91eb0..a7bf139b 100644 --- a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/AuthenticateAPI.kt +++ b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/AuthenticateAPI.kt @@ -38,6 +38,9 @@ interface AuthenticateAPI { data class Result(val binding: Binding, val targetAmount: Int) { val percentage: Int get() = binding.percentage(targetAmount) + + val acceptable: Boolean + get() = binding.paths.amount >= targetAmount } } diff --git a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/Binding.kt b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/Binding.kt index 7c5e94fd..f79dbe66 100644 --- a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/Binding.kt +++ b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/Binding.kt @@ -16,34 +16,4 @@ data class Binding(val fingerprint: Fingerprint, val userId: String, val paths: fun percentage(targetAmount: Int): Int { return paths.amount * 100 / targetAmount } - - fun toConsoleOut(targetAmount: Int, dateFormat: SimpleDateFormat): String { - return buildString { - val percentage = percentage(targetAmount) - val authLevel = when (paths.amount) { - in 0..39 -> "not authenticated" - in 40..119 -> "partially authenticated" - in 120 .. 239 -> "fully authenticated" - else -> {if (percentage < 0) "not authenticated" else "doubly authenticated"} - } - append(if (percentage >= 100) "[✓] " else "[ ] ") - appendLine("$fingerprint $userId: $authLevel (${percentage(targetAmount)}%)") - for ((pIndex, path: Path) in paths.paths.withIndex()) { - appendLine(" Path #${pIndex + 1} of ${paths.paths.size}, trust amount ${path.amount}:") - for ((cIndex, certification) in path.certifications.withIndex()) { - val issuerUserId = certification.issuer.userIds.keys.firstOrNull()?.let { " (\"$it\")" } ?: "" - when (cIndex) { - 0 -> { - appendLine(" ◯ ${certification.issuer.fingerprint}$issuerUserId") - } - else -> { - appendLine(" ├ ${certification.issuer.fingerprint}$issuerUserId") - } - } - appendLine(" │ certified the following binding on ${dateFormat.format(certification.creationTime)}") - } - appendLine(" └ $fingerprint \"$userId\"") - } - } - } } \ No newline at end of file diff --git a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/IdentifyAPI.kt b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/IdentifyAPI.kt index 1a189496..8429455e 100644 --- a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/IdentifyAPI.kt +++ b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/IdentifyAPI.kt @@ -14,5 +14,10 @@ interface IdentifyAPI { data class Arguments(val fingerprint: Fingerprint) - data class Result(val bindings: List, val targetAmount: Int) + data class Result(val bindings: List, val targetAmount: Int) { + val acceptable: Boolean + get() = bindings.any { + it.paths.amount >= targetAmount + } + } } diff --git a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/ListAPI.kt b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/ListAPI.kt index 1f8be058..e0e0b8ef 100644 --- a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/ListAPI.kt +++ b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/ListAPI.kt @@ -8,5 +8,5 @@ interface ListAPI { fun list(): Result - data class Result(val bindings: List) + data class Result(val bindings: List, val targetAmount: Int) } diff --git a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/LookupAPI.kt b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/LookupAPI.kt index 5ad47955..4c30d383 100644 --- a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/LookupAPI.kt +++ b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/LookupAPI.kt @@ -10,5 +10,10 @@ interface LookupAPI { data class Arguments(val userId: String, val email: Boolean = false) - data class Result(val bindings: List) + data class Result(val bindings: List, val targetAmount: Int) { + val acceptable: Boolean + get() = bindings.any { + it.paths.amount >= targetAmount + } + } } diff --git a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/WoTAPI.kt b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/WoTAPI.kt index 13b77e59..e9373cd7 100644 --- a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/WoTAPI.kt +++ b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/WoTAPI.kt @@ -64,15 +64,13 @@ class WoTAPI( network.nodes.forEach { bindings.addAll(identify(IdentifyAPI.Arguments(it.key)).bindings) } - return ListAPI.Result(bindings) + return ListAPI.Result(bindings, trustAmount) } override fun lookup(arguments: LookupAPI.Arguments): LookupAPI.Result { val userId = arguments.userId val email = arguments.email - println("Looking up $userId email=$email") - val candidates = network.nodes.values.mapNotNull { node -> val matches = node.mapToMatchingUserIds(userId, email) if (matches.isEmpty()) { @@ -82,11 +80,6 @@ class WoTAPI( } } - println("found ${candidates.size} candidates:") - candidates.joinToString { - "${it.first.fingerprint} ${it.second.joinToString { u -> u }}" - } - val results = mutableListOf() candidates.forEach { val node = it.first @@ -101,7 +94,7 @@ class WoTAPI( } } - return LookupAPI.Result(results) + return LookupAPI.Result(results, trustAmount) } override fun path(arguments: PathAPI.Arguments): PathAPI.Result {