diff --git a/pgpainless-wot-cli/build.gradle b/pgpainless-wot-cli/build.gradle index 6ff93b76..6d7f5573 100644 --- a/pgpainless-wot-cli/build.gradle +++ b/pgpainless-wot-cli/build.gradle @@ -36,12 +36,13 @@ dependencies { // Logging testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + implementation "ch.qos.logback:logback-classic:$logbackVersion" // Web of Trust implementation(project(":pgpainless-wot")) // Web Key Directory - implementation("org.pgpainless:wkd-java:0.1.1") + // implementation("org.pgpainless:wkd-java:0.1.2") } mainClassName = 'org.pgpainless.wot.cli.WotCLI' @@ -50,21 +51,6 @@ application { mainClass = mainClassName } -jar { - duplicatesStrategy(DuplicatesStrategy.EXCLUDE) - manifest { - attributes 'Main-Class': "$mainClassName" - } - - from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } { - exclude "META-INF/*.SF" - exclude "META-INF/*.DSA" - exclude "META-INF/*.RSA" - } -} - test { useJUnitPlatform() } 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 4d27cb81..17224731 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,16 +9,22 @@ 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.cli.format.Formatter import org.pgpainless.wot.api.WoTAPI +import org.pgpainless.wot.cli.format.SQWOTFormatter 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 pgp.cert_d.PGPCertificateStoreAdapter +import pgp.cert_d.SpecialNames import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory import pgp.certificate_store.PGPCertificateStore import picocli.CommandLine import picocli.CommandLine.* import java.io.File +import java.text.SimpleDateFormat import java.util.concurrent.Callable import kotlin.system.exitProcess @@ -39,7 +45,7 @@ import kotlin.system.exitProcess ) class WotCLI: Callable { - @Option(names = ["--trust-root", "-r"], required = true) + @Option(names = ["--trust-root", "-r"]) var mTrustRoot: Array = arrayOf() @ArgGroup(exclusive = true, multiplicity = "1") @@ -62,10 +68,10 @@ class WotCLI: Callable { @Option(names = ["--keyserver"], description=["Change the default keyserver"]) var keyServer: String = "hkps://keyserver.ubuntu.com" + */ @Option(names = ["--gpg-ownertrust"]) var gpgOwnertrust: Boolean = false - */ @Option(names = ["--certification-network"], description = ["Treat the web of trust as a certification network instead of an authentication network."]) var certificationNetwork = false @@ -73,8 +79,8 @@ class WotCLI: Callable { @Option(names = ["--gossip"], description = ["Find arbitrary paths by treating all certificates as trust-roots with zero trust."]) var gossip = false - @ArgGroup(exclusive = true, multiplicity = "1") - lateinit var mTrustAmount: TrustAmount + @ArgGroup(exclusive = true) + var mTrustAmount: TrustAmount = TrustAmount() class TrustAmount { @Option(names = ["--trust-amount", "-a"], description = ["The required amount of trust."]) @@ -104,22 +110,30 @@ class WotCLI: Callable { } ?: ReferenceTime.now() } - private val trustRoots: List + private val trustRoots: Roots get() { - if (mCertificateSource.gpg) { - return readGpgOwnertrust().plus(mTrustRoot.map { Fingerprint(it) }) + var trustRootFingerprints = mTrustRoot.map { Fingerprint(it) }.map { Root(it) } + if (mCertificateSource.gpg || gpgOwnertrust) { + trustRootFingerprints = trustRootFingerprints.plus(readGpgOwnertrust()) } - - return mTrustRoot.map { Fingerprint(it) } + if (mCertificateSource.pgpCertD != null) { + try { + val rootCert = certificateStore.getCertificate(SpecialNames.TRUST_ROOT) + trustRootFingerprints = trustRootFingerprints.plus(Root(Fingerprint(rootCert.fingerprint), Int.MAX_VALUE)) + } catch (e: NoSuchElementException) { + // ignore + } + } + return Roots(trustRootFingerprints) } private val amount: Int get() = when { - mTrustAmount.amount != null -> mTrustAmount.amount!! // --amount=XY + mTrustAmount.amount != null -> mTrustAmount.amount!! // --amount=XY mTrustAmount.partial -> 40 // --partial mTrustAmount.full -> 120 // --full mTrustAmount.double -> 240 // --double - else -> if (certificationNetwork) 1200 else 120 // default 120, if --certification-network -> 1200 + else -> if (certificationNetwork) 1200 else 120 // default 120, if --certification-network -> 1200 } private val certificateStore: PGPCertificateStore @@ -145,15 +159,33 @@ class WotCLI: Callable { return PGPCertificateStoreAdapter(certD) } - fun readGpgOwnertrust(): List = Runtime.getRuntime() + val formatter: Formatter = SQWOTFormatter() + + fun readGpgOwnertrust(): List = Runtime.getRuntime() .exec("/usr/bin/gpg --export-ownertrust") .inputStream .bufferedReader() .readLines() + .asSequence() .filterNot { it.startsWith("#") } .filterNot { it.isBlank() } - .map { it.substring(0, it.indexOf(':')) } - .map { Fingerprint(it) } + .map { + Fingerprint(it.substring(0, it.indexOf(':'))) to it.elementAt(it.indexOf(':') + 1) } + .map { + it.first to when (it.second.digitToInt()) { + 2 -> null // unknown + 3 -> 0 // not trust + 4 -> 40 // marginally trusted + 5 -> 120 // fully trusted + 6 -> Int.MAX_VALUE // ultimately trusted + else -> null + } + } + .filterNot { it.second == null } + .map { + Root(it.first, it.second!!) + } + .toList() /** * Execute the command. @@ -192,6 +224,8 @@ class WotCLI: Callable { CommandLine(WotCLI()).execute(*args) ) + @JvmStatic + val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd") } override fun toString(): String { 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/SQWOTFormatter.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/format/SQWOTFormatter.kt new file mode 100644 index 00000000..c636aa72 --- /dev/null +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/format/SQWOTFormatter.kt @@ -0,0 +1,69 @@ +// 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 SQWOTFormatter: 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("│ ${certDegree(edge.trustAmount)} the following " + + (if (edge.userId == null) "binding" else "certificate") + + " on ${dateFormat.format(edge.creationTime)}" + + (if (edge.expirationTime == null) "" else " (expiry: ${dateFormat.format(edge.expirationTime)})") + + " as a TODO trusted TODO-introducer (depth: ${edge.trustDepth.value()})" + ) + + append(indent); append(if (eIndex != path.certifications.lastIndex) "├ " else "└ ") + appendLine("${edge.target.fingerprint}$targetUserId") + } + if (pIndex != pathList.lastIndex) { + appendLine() + } + } + } + } + + private fun certDegree(amount: Int): String { + return when (amount) { + in 1 .. 119 -> "partially certified (amount: $amount of 120) " + else -> if (amount <= 0) "did not certify (amount: $amount of 120) " else "certified " + } + } +} \ 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 2258a964..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 - private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd") - /** * Execute the command. * @return exit code @@ -54,40 +52,8 @@ class AuthenticateCmd: Callable { override fun call(): Int { val result = parent.api.authenticate(AuthenticateAPI.Arguments( Fingerprint(fingerprint), userId, email)) - print(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): String { - if (result.percentage < 100) { - return "No paths found." - } - - val sb = StringBuilder() - sb.appendLine("[✓] ${result.fingerprint} ${result.userId}: fully authenticated (${result.percentage}%)") - for ((pIndex, path: Path) in result.paths.paths.withIndex()) { - sb.appendLine(" Path #${pIndex + 1} of ${result.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 -> { - sb.appendLine(" ◯ ${certification.issuer.fingerprint}${issuerUserId}") - } - else -> { - sb.appendLine(" ├ ${certification.issuer.fingerprint}${issuerUserId}") - } - } - sb.appendLine(" │ certified the following binding on ${dateFormat.format(certification.creationTime)}") - } - sb.appendLine(" └ ${result.fingerprint} \"${result.userId}\"") - } - - return sb.toString() + 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 0011d30e..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 @@ -4,10 +4,13 @@ package org.pgpainless.wot.cli.subcommands +import org.pgpainless.wot.api.IdentifyAPI import org.pgpainless.wot.cli.WotCLI +import org.pgpainless.wot.network.Fingerprint import picocli.CommandLine import picocli.CommandLine.Command import picocli.CommandLine.Parameters +import java.text.SimpleDateFormat import java.util.concurrent.Callable @Command(name = "identify") @@ -25,8 +28,9 @@ class IdentifyCmd: Callable { * @return exit code */ override fun call(): Int { - val api = parent.api - TODO("Not yet implemented") - } + val result = parent.api.identify(IdentifyAPI.Arguments(Fingerprint(fingerprint))) + 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/main/kotlin/org/pgpainless/wot/cli/subcommands/ListCmd.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/ListCmd.kt index 978d0cbb..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,7 +21,9 @@ class ListCmd: Callable { * @return exit code */ override fun call(): Int { - val api = parent.api - TODO("Not yet implemented") + 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 4725b9e1..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 @@ -4,6 +4,7 @@ package org.pgpainless.wot.cli.subcommands +import org.pgpainless.wot.api.LookupAPI import org.pgpainless.wot.cli.WotCLI import picocli.CommandLine.* import java.util.concurrent.Callable @@ -26,7 +27,9 @@ class LookupCmd: Callable { * @return exit code */ override fun call(): Int { - val api = parent.api - TODO("Not yet implemented") + 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/main/resources/logback.xml b/pgpainless-wot-cli/src/main/resources/logback.xml new file mode 100644 index 00000000..0049b71b --- /dev/null +++ b/pgpainless-wot-cli/src/main/resources/logback.xml @@ -0,0 +1,26 @@ + + + + + System.err + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + System.out + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/pgpainless-wot-cli/src/test/kotlin/org/pgpainless/wot/cli/format/SQWOTFormatterTest.kt b/pgpainless-wot-cli/src/test/kotlin/org/pgpainless/wot/cli/format/SQWOTFormatterTest.kt new file mode 100644 index 00000000..1aec5851 --- /dev/null +++ b/pgpainless-wot-cli/src/test/kotlin/org/pgpainless/wot/cli/format/SQWOTFormatterTest.kt @@ -0,0 +1,55 @@ +package org.pgpainless.wot.cli.format + +import org.junit.jupiter.api.Test +import org.pgpainless.wot.api.AuthenticateAPI +import org.pgpainless.wot.api.Binding +import org.pgpainless.wot.network.* +import org.pgpainless.wot.query.Path +import org.pgpainless.wot.query.Paths +import java.text.SimpleDateFormat +import kotlin.test.assertEquals + +class SQWOTFormatterTest { + private val dateFormat = SimpleDateFormat("yyyy-MM-dd") + private val formatter = SQWOTFormatter() as Formatter + + private val nodeAlice = Node(fingerprint = Fingerprint("A".repeat(40)), + userIds = mapOf("Alice " to RevocationState.notRevoked())) + private val nodeBob = Node(fingerprint = Fingerprint("B".repeat(40))) + private val nodeCharlie = Node(fingerprint = Fingerprint("C".repeat(40)), + userIds = mapOf("Charlie " to RevocationState.notRevoked())) + + @Test + fun `testFormattingOfAuthenticateResult`() { + val targetAmount = 120 + val binding = Binding( + nodeAlice.fingerprint, + "Alice ", + Paths().apply { + add( + Path(nodeBob, mutableListOf((EdgeComponent( + nodeBob, + nodeAlice, + "Alice ", + dateFormat.parse("2023-01-01"), + null, + true, + 120, + Depth.auto(10), + RegexSet.wildcard()) + )), + Depth.auto(9)), + 120) + } + ) + val result = AuthenticateAPI.Result(binding, targetAmount) + val output = formatter.format(result) + + assertEquals(""" +[✓] AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Alice : fully authenticated (100%) + ◯ BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB + │ certified the following binding on 2023-01-01 + └ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA "Alice " +""".trimStart(), output) + } +} \ 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 072371aa..05bbc54c 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 @@ -6,6 +6,8 @@ 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.SQWOTFormatter import org.pgpainless.wot.network.* import org.pgpainless.wot.query.Path import org.pgpainless.wot.query.Paths @@ -17,7 +19,6 @@ class AuthenticateCmdTest { @Test fun testFormatting() { val dateFormat = SimpleDateFormat("yyyy-MM-dd") - val cmd = AuthenticateCmd() val paths = Paths() val neal = Node( Fingerprint("F7173B3C7C685CD9ECC4191B74E445BA0E15C957"), @@ -46,19 +47,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 = SQWOTFormatter().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/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/AuthenticateAPI.kt b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/AuthenticateAPI.kt index e90f8008..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 @@ -5,7 +5,6 @@ package org.pgpainless.wot.api import org.pgpainless.wot.network.Fingerprint -import org.pgpainless.wot.query.Paths /** * Authenticate a binding. @@ -36,13 +35,12 @@ interface AuthenticateAPI { * @param targetAmount the targeted trust amount required to achieve full authentication * @param paths the number of paths */ - data class Result(val fingerprint: Fingerprint, val userId: String, private val targetAmount: Int, val paths: Paths) { - - /** - * Percentage of authentication. 100% means fully authenticated binding. - */ + data class Result(val binding: Binding, val targetAmount: Int) { val percentage: Int - get() = paths.amount * 100 / targetAmount + 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 new file mode 100644 index 00000000..f79dbe66 --- /dev/null +++ b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/api/Binding.kt @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.wot.api + +import org.pgpainless.wot.network.Fingerprint +import org.pgpainless.wot.query.Path +import org.pgpainless.wot.query.Paths +import java.text.SimpleDateFormat + +data class Binding(val fingerprint: Fingerprint, val userId: String, val paths: Paths) { + /** + * Percentage of authentication. 100% means fully authenticated binding. + */ + fun percentage(targetAmount: Int): Int { + return paths.amount * 100 / targetAmount + } +} \ 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 67e3809e..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 @@ -5,6 +5,7 @@ package org.pgpainless.wot.api import org.pgpainless.wot.network.Fingerprint +import org.pgpainless.wot.network.Node import org.pgpainless.wot.query.Paths interface IdentifyAPI { @@ -13,5 +14,10 @@ interface IdentifyAPI { data class Arguments(val fingerprint: Fingerprint) - data class Result(val paths: Paths) + 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 3c321d50..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 @@ -4,11 +4,9 @@ package org.pgpainless.wot.api -import org.pgpainless.wot.query.Paths - interface ListAPI { fun list(): Result - data class Result(val paths: Paths) + 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 bb4f5725..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 @@ -4,13 +4,16 @@ package org.pgpainless.wot.api -import org.pgpainless.wot.query.Paths - interface LookupAPI { fun lookup(arguments: Arguments): Result data class Arguments(val userId: String, val email: Boolean = false) - data class Result(val paths: Paths) + 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 9dc11e78..e02f07f3 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 @@ -4,9 +4,9 @@ package org.pgpainless.wot.api -import org.pgpainless.wot.network.Fingerprint -import org.pgpainless.wot.network.Network -import org.pgpainless.wot.network.ReferenceTime +import org.pgpainless.wot.query.Query +import org.pgpainless.wot.network.* +import org.pgpainless.wot.query.Paths /** * Web of Trust API, offering different operations. @@ -17,11 +17,10 @@ import org.pgpainless.wot.network.ReferenceTime * @param certificationNetwork if true, all certifications are treated as delegations with infinite trust depth and no regular expressions * @param trustAmount minimum trust amount * @param referenceTime reference time at which the web of trust is evaluated - * @param knownNotationRegistry registry of known notations */ class WoTAPI( val network: Network, - val trustRoots: List, + val trustRoots: Roots, val gossip: Boolean = false, val certificationNetwork: Boolean = false, val trustAmount: Int = AuthenticationLevel.Fully.amount, @@ -32,7 +31,7 @@ class WoTAPI( * Secondary constructor, taking an [AuthenticationLevel] instead of an [Int]. */ constructor(network: Network, - trustRoots: List, + trustRoots: Roots, gossip: Boolean = false, certificationNetwork: Boolean = false, trustAmount: AuthenticationLevel = AuthenticationLevel.Fully, @@ -40,23 +39,82 @@ class WoTAPI( this(network,trustRoots, gossip,certificationNetwork, trustAmount.amount, referenceTime) override fun authenticate(arguments: AuthenticateAPI.Arguments): AuthenticateAPI.Result { - TODO("Not yet implemented") + val query = Query(network, trustRoots, certificationNetwork) + val paths = query.authenticate(arguments.fingerprint, arguments.userId, trustAmount) + return AuthenticateAPI.Result(Binding(arguments.fingerprint, arguments.userId, paths), trustAmount) } override fun identify(arguments: IdentifyAPI.Arguments): IdentifyAPI.Result { - TODO("Not yet implemented") + val cert = network.nodes[arguments.fingerprint] + ?: return IdentifyAPI.Result(listOf(), trustAmount) + + val bindings = mutableListOf() + cert.userIds.keys.toList().forEach { + val query = Query(network, trustRoots, certificationNetwork) + val paths = query.authenticate(arguments.fingerprint, it, trustAmount) + if (paths.amount != 0) { + bindings.add(Binding(arguments.fingerprint, it, paths)) + } + } + return IdentifyAPI.Result(bindings, trustAmount) } override fun list(): ListAPI.Result { - TODO("Not yet implemented") + val bindings = mutableListOf() + network.nodes.forEach { + bindings.addAll(identify(IdentifyAPI.Arguments(it.key)).bindings) + } + return ListAPI.Result(bindings, trustAmount) } override fun lookup(arguments: LookupAPI.Arguments): LookupAPI.Result { - TODO("Not yet implemented") + val userId = arguments.userId + val email = arguments.email + + val candidates = network.nodes.values.mapNotNull { node -> + val matches = node.mapToMatchingUserIds(userId, email) + if (matches.isEmpty()) { + null + } else { + node to matches + } + } + + val results = mutableListOf() + candidates.forEach { + val node = it.first + val userIds = it.second + + for (mUserId in userIds) { + authenticate(AuthenticateAPI.Arguments(node.fingerprint, mUserId, email)).let { result -> + if (result.binding.paths.paths.isNotEmpty()) { + results.add(result.binding) + } + } + } + } + + return LookupAPI.Result(results, trustAmount) } override fun path(arguments: PathAPI.Arguments): PathAPI.Result { TODO("Not yet implemented") } + private fun Node.mapToMatchingUserIds(userId: String, email: Boolean): List { + val list = mutableListOf() + userIds.forEach { entry -> + if (email) { + if (entry.key.contains("<$userId>")) { + list.add(entry.key) + } + } else { + if (entry.key == userId) { + list.add(entry.key) + } + } + } + return list + } + } \ No newline at end of file