mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-12-25 04:17:59 +01:00
Implement API and CLI
This commit is contained in:
parent
5f447dc9d5
commit
c6d5a5434a
17 changed files with 392 additions and 107 deletions
|
@ -36,12 +36,13 @@ dependencies {
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
|
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
|
||||||
|
implementation "ch.qos.logback:logback-classic:$logbackVersion"
|
||||||
|
|
||||||
// Web of Trust
|
// Web of Trust
|
||||||
implementation(project(":pgpainless-wot"))
|
implementation(project(":pgpainless-wot"))
|
||||||
|
|
||||||
// Web Key Directory
|
// 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'
|
mainClassName = 'org.pgpainless.wot.cli.WotCLI'
|
||||||
|
@ -50,21 +51,6 @@ application {
|
||||||
mainClass = mainClassName
|
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 {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,16 +9,22 @@ import org.pgpainless.certificate_store.PGPainlessCertD
|
||||||
import org.pgpainless.util.DateUtil
|
import org.pgpainless.util.DateUtil
|
||||||
import org.pgpainless.wot.KeyRingCertificateStore
|
import org.pgpainless.wot.KeyRingCertificateStore
|
||||||
import org.pgpainless.wot.WebOfTrust
|
import org.pgpainless.wot.WebOfTrust
|
||||||
|
import org.pgpainless.wot.cli.format.Formatter
|
||||||
import org.pgpainless.wot.api.WoTAPI
|
import org.pgpainless.wot.api.WoTAPI
|
||||||
|
import org.pgpainless.wot.cli.format.SQWOTFormatter
|
||||||
import org.pgpainless.wot.cli.subcommands.*
|
import org.pgpainless.wot.cli.subcommands.*
|
||||||
import org.pgpainless.wot.network.Fingerprint
|
import org.pgpainless.wot.network.Fingerprint
|
||||||
import org.pgpainless.wot.network.ReferenceTime
|
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.PGPCertificateStoreAdapter
|
||||||
|
import pgp.cert_d.SpecialNames
|
||||||
import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory
|
import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory
|
||||||
import pgp.certificate_store.PGPCertificateStore
|
import pgp.certificate_store.PGPCertificateStore
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import picocli.CommandLine.*
|
import picocli.CommandLine.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
@ -39,7 +45,7 @@ import kotlin.system.exitProcess
|
||||||
)
|
)
|
||||||
class WotCLI: Callable<Int> {
|
class WotCLI: Callable<Int> {
|
||||||
|
|
||||||
@Option(names = ["--trust-root", "-r"], required = true)
|
@Option(names = ["--trust-root", "-r"])
|
||||||
var mTrustRoot: Array<String> = arrayOf()
|
var mTrustRoot: Array<String> = arrayOf()
|
||||||
|
|
||||||
@ArgGroup(exclusive = true, multiplicity = "1")
|
@ArgGroup(exclusive = true, multiplicity = "1")
|
||||||
|
@ -62,10 +68,10 @@ class WotCLI: Callable<Int> {
|
||||||
|
|
||||||
@Option(names = ["--keyserver"], description=["Change the default keyserver"])
|
@Option(names = ["--keyserver"], description=["Change the default keyserver"])
|
||||||
var keyServer: String = "hkps://keyserver.ubuntu.com"
|
var keyServer: String = "hkps://keyserver.ubuntu.com"
|
||||||
|
*/
|
||||||
|
|
||||||
@Option(names = ["--gpg-ownertrust"])
|
@Option(names = ["--gpg-ownertrust"])
|
||||||
var gpgOwnertrust: Boolean = false
|
var gpgOwnertrust: Boolean = false
|
||||||
*/
|
|
||||||
|
|
||||||
@Option(names = ["--certification-network"], description = ["Treat the web of trust as a certification network instead of an authentication network."])
|
@Option(names = ["--certification-network"], description = ["Treat the web of trust as a certification network instead of an authentication network."])
|
||||||
var certificationNetwork = false
|
var certificationNetwork = false
|
||||||
|
@ -73,8 +79,8 @@ class WotCLI: Callable<Int> {
|
||||||
@Option(names = ["--gossip"], description = ["Find arbitrary paths by treating all certificates as trust-roots with zero trust."])
|
@Option(names = ["--gossip"], description = ["Find arbitrary paths by treating all certificates as trust-roots with zero trust."])
|
||||||
var gossip = false
|
var gossip = false
|
||||||
|
|
||||||
@ArgGroup(exclusive = true, multiplicity = "1")
|
@ArgGroup(exclusive = true)
|
||||||
lateinit var mTrustAmount: TrustAmount
|
var mTrustAmount: TrustAmount = TrustAmount()
|
||||||
|
|
||||||
class TrustAmount {
|
class TrustAmount {
|
||||||
@Option(names = ["--trust-amount", "-a"], description = ["The required amount of trust."])
|
@Option(names = ["--trust-amount", "-a"], description = ["The required amount of trust."])
|
||||||
|
@ -104,22 +110,30 @@ class WotCLI: Callable<Int> {
|
||||||
} ?: ReferenceTime.now()
|
} ?: ReferenceTime.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val trustRoots: List<Fingerprint>
|
private val trustRoots: Roots
|
||||||
get() {
|
get() {
|
||||||
if (mCertificateSource.gpg) {
|
var trustRootFingerprints = mTrustRoot.map { Fingerprint(it) }.map { Root(it) }
|
||||||
return readGpgOwnertrust().plus(mTrustRoot.map { Fingerprint(it) })
|
if (mCertificateSource.gpg || gpgOwnertrust) {
|
||||||
|
trustRootFingerprints = trustRootFingerprints.plus(readGpgOwnertrust())
|
||||||
}
|
}
|
||||||
|
if (mCertificateSource.pgpCertD != null) {
|
||||||
return mTrustRoot.map { Fingerprint(it) }
|
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
|
private val amount: Int
|
||||||
get() = when {
|
get() = when {
|
||||||
mTrustAmount.amount != null -> mTrustAmount.amount!! // --amount=XY
|
mTrustAmount.amount != null -> mTrustAmount.amount!! // --amount=XY
|
||||||
mTrustAmount.partial -> 40 // --partial
|
mTrustAmount.partial -> 40 // --partial
|
||||||
mTrustAmount.full -> 120 // --full
|
mTrustAmount.full -> 120 // --full
|
||||||
mTrustAmount.double -> 240 // --double
|
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
|
private val certificateStore: PGPCertificateStore
|
||||||
|
@ -145,15 +159,33 @@ class WotCLI: Callable<Int> {
|
||||||
return PGPCertificateStoreAdapter(certD)
|
return PGPCertificateStoreAdapter(certD)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readGpgOwnertrust(): List<Fingerprint> = Runtime.getRuntime()
|
val formatter: Formatter = SQWOTFormatter()
|
||||||
|
|
||||||
|
fun readGpgOwnertrust(): List<Root> = Runtime.getRuntime()
|
||||||
.exec("/usr/bin/gpg --export-ownertrust")
|
.exec("/usr/bin/gpg --export-ownertrust")
|
||||||
.inputStream
|
.inputStream
|
||||||
.bufferedReader()
|
.bufferedReader()
|
||||||
.readLines()
|
.readLines()
|
||||||
|
.asSequence()
|
||||||
.filterNot { it.startsWith("#") }
|
.filterNot { it.startsWith("#") }
|
||||||
.filterNot { it.isBlank() }
|
.filterNot { it.isBlank() }
|
||||||
.map { it.substring(0, it.indexOf(':')) }
|
.map {
|
||||||
.map { Fingerprint(it) }
|
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.
|
* Execute the command.
|
||||||
|
@ -192,6 +224,8 @@ class WotCLI: Callable<Int> {
|
||||||
CommandLine(WotCLI()).execute(*args)
|
CommandLine(WotCLI()).execute(*args)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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 <vanitasvitae@fsfe.org>: 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 "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,8 +45,6 @@ class AuthenticateCmd: Callable<Int> {
|
||||||
@CommandLine.Option(names = ["--email"], description = ["Consider all user-IDs that contain the given email address."])
|
@CommandLine.Option(names = ["--email"], description = ["Consider all user-IDs that contain the given email address."])
|
||||||
var email = false
|
var email = false
|
||||||
|
|
||||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the command.
|
* Execute the command.
|
||||||
* @return exit code
|
* @return exit code
|
||||||
|
@ -54,40 +52,8 @@ class AuthenticateCmd: Callable<Int> {
|
||||||
override fun call(): Int {
|
override fun call(): Int {
|
||||||
val result = parent.api.authenticate(AuthenticateAPI.Arguments(
|
val result = parent.api.authenticate(AuthenticateAPI.Arguments(
|
||||||
Fingerprint(fingerprint), userId, email))
|
Fingerprint(fingerprint), userId, email))
|
||||||
print(formatResult(result))
|
|
||||||
if (result.percentage < 100) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
println(parent.formatter.format(result))
|
||||||
* Format the [AuthenticateAPI.Result] as a [String] which can be printed to standard out.
|
return if (result.acceptable) 0 else 1
|
||||||
*/
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,10 +4,13 @@
|
||||||
|
|
||||||
package org.pgpainless.wot.cli.subcommands
|
package org.pgpainless.wot.cli.subcommands
|
||||||
|
|
||||||
|
import org.pgpainless.wot.api.IdentifyAPI
|
||||||
import org.pgpainless.wot.cli.WotCLI
|
import org.pgpainless.wot.cli.WotCLI
|
||||||
|
import org.pgpainless.wot.network.Fingerprint
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import picocli.CommandLine.Command
|
import picocli.CommandLine.Command
|
||||||
import picocli.CommandLine.Parameters
|
import picocli.CommandLine.Parameters
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
|
|
||||||
@Command(name = "identify")
|
@Command(name = "identify")
|
||||||
|
@ -25,8 +28,9 @@ class IdentifyCmd: Callable<Int> {
|
||||||
* @return exit code
|
* @return exit code
|
||||||
*/
|
*/
|
||||||
override fun call(): Int {
|
override fun call(): Int {
|
||||||
val api = parent.api
|
val result = parent.api.identify(IdentifyAPI.Arguments(Fingerprint(fingerprint)))
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
print(parent.formatter.format(result))
|
||||||
|
return if (result.acceptable) 0 else 1
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -21,7 +21,9 @@ class ListCmd: Callable<Int> {
|
||||||
* @return exit code
|
* @return exit code
|
||||||
*/
|
*/
|
||||||
override fun call(): Int {
|
override fun call(): Int {
|
||||||
val api = parent.api
|
val result = parent.api.list()
|
||||||
TODO("Not yet implemented")
|
|
||||||
|
println(parent.formatter.format(result))
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
package org.pgpainless.wot.cli.subcommands
|
package org.pgpainless.wot.cli.subcommands
|
||||||
|
|
||||||
|
import org.pgpainless.wot.api.LookupAPI
|
||||||
import org.pgpainless.wot.cli.WotCLI
|
import org.pgpainless.wot.cli.WotCLI
|
||||||
import picocli.CommandLine.*
|
import picocli.CommandLine.*
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
|
@ -26,7 +27,9 @@ class LookupCmd: Callable<Int> {
|
||||||
* @return exit code
|
* @return exit code
|
||||||
*/
|
*/
|
||||||
override fun call(): Int {
|
override fun call(): Int {
|
||||||
val api = parent.api
|
val result = parent.api.lookup(LookupAPI.Arguments(userId, email))
|
||||||
TODO("Not yet implemented")
|
|
||||||
|
print(parent.formatter.format(result))
|
||||||
|
return if (result.acceptable) 0 else 1
|
||||||
}
|
}
|
||||||
}
|
}
|
26
pgpainless-wot-cli/src/main/resources/logback.xml
Normal file
26
pgpainless-wot-cli/src/main/resources/logback.xml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
<configuration debug="false">
|
||||||
|
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<target>System.err</target>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<target>System.out</target>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="error">
|
||||||
|
<appender-ref ref="STDERR" />
|
||||||
|
</root>
|
||||||
|
|
||||||
|
</configuration>
|
|
@ -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 <alice@pgpainless.org>" to RevocationState.notRevoked()))
|
||||||
|
private val nodeBob = Node(fingerprint = Fingerprint("B".repeat(40)))
|
||||||
|
private val nodeCharlie = Node(fingerprint = Fingerprint("C".repeat(40)),
|
||||||
|
userIds = mapOf("Charlie <charlie@example.org>" to RevocationState.notRevoked()))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `testFormattingOfAuthenticateResult`() {
|
||||||
|
val targetAmount = 120
|
||||||
|
val binding = Binding(
|
||||||
|
nodeAlice.fingerprint,
|
||||||
|
"Alice <alice@pgpainless.org>",
|
||||||
|
Paths().apply {
|
||||||
|
add(
|
||||||
|
Path(nodeBob, mutableListOf((EdgeComponent(
|
||||||
|
nodeBob,
|
||||||
|
nodeAlice,
|
||||||
|
"Alice <alice@pgpainless.org>",
|
||||||
|
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 <alice@pgpainless.org>: fully authenticated (100%)
|
||||||
|
◯ BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||||
|
│ certified the following binding on 2023-01-01
|
||||||
|
└ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA "Alice <alice@pgpainless.org>"
|
||||||
|
""".trimStart(), output)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ package org.pgpainless.wot.cli.subcommands
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.pgpainless.wot.api.AuthenticateAPI
|
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.network.*
|
||||||
import org.pgpainless.wot.query.Path
|
import org.pgpainless.wot.query.Path
|
||||||
import org.pgpainless.wot.query.Paths
|
import org.pgpainless.wot.query.Paths
|
||||||
|
@ -17,7 +19,6 @@ class AuthenticateCmdTest {
|
||||||
@Test
|
@Test
|
||||||
fun testFormatting() {
|
fun testFormatting() {
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
|
||||||
val cmd = AuthenticateCmd()
|
|
||||||
val paths = Paths()
|
val paths = Paths()
|
||||||
val neal = Node(
|
val neal = Node(
|
||||||
Fingerprint("F7173B3C7C685CD9ECC4191B74E445BA0E15C957"),
|
Fingerprint("F7173B3C7C685CD9ECC4191B74E445BA0E15C957"),
|
||||||
|
@ -46,19 +47,18 @@ class AuthenticateCmdTest {
|
||||||
Depth.limited(0),
|
Depth.limited(0),
|
||||||
RegexSet.wildcard())
|
RegexSet.wildcard())
|
||||||
paths.add(Path(neal, mutableListOf(edgeComponent), Depth.auto(0)), 120)
|
paths.add(Path(neal, mutableListOf(edgeComponent), Depth.auto(0)), 120)
|
||||||
val testResult = AuthenticateAPI.Result(
|
val testResult = AuthenticateAPI.Result(Binding(
|
||||||
Fingerprint("CBCD8F030588653EEDD7E2659B7DD433F254904A"),
|
Fingerprint("CBCD8F030588653EEDD7E2659B7DD433F254904A"),
|
||||||
"Justus Winter <justus@sequoia-pgp.org>",
|
"Justus Winter <justus@sequoia-pgp.org>",
|
||||||
120,
|
paths),
|
||||||
paths)
|
120, )
|
||||||
|
|
||||||
val formatted = cmd.formatResult(testResult)
|
val formatted = SQWOTFormatter().format(testResult)
|
||||||
assertEquals(buildString {
|
assertEquals(buildString {
|
||||||
append("[✓] CBCD8F030588653EEDD7E2659B7DD433F254904A Justus Winter <justus@sequoia-pgp.org>: fully authenticated (100%)\n")
|
appendLine("[✓] CBCD8F030588653EEDD7E2659B7DD433F254904A Justus Winter <justus@sequoia-pgp.org>: fully authenticated (100%)")
|
||||||
append(" Path #1 of 1, trust amount 120:\n")
|
appendLine(" ◯ F7173B3C7C685CD9ECC4191B74E445BA0E15C957 (\"Neal H. Walfield (Code Signing Key) <neal@pep.foundation>\")")
|
||||||
append(" ◯ F7173B3C7C685CD9ECC4191B74E445BA0E15C957 (\"Neal H. Walfield (Code Signing Key) <neal@pep.foundation>\")\n")
|
appendLine(" │ certified the following binding on 2022-02-04")
|
||||||
append(" │ certified the following binding on 2022-02-04\n")
|
appendLine(" └ CBCD8F030588653EEDD7E2659B7DD433F254904A \"Justus Winter <justus@sequoia-pgp.org>\"")
|
||||||
append(" └ CBCD8F030588653EEDD7E2659B7DD433F254904A \"Justus Winter <justus@sequoia-pgp.org>\"\n")
|
|
||||||
}, formatted)
|
}, formatted)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,7 +5,6 @@
|
||||||
package org.pgpainless.wot.api
|
package org.pgpainless.wot.api
|
||||||
|
|
||||||
import org.pgpainless.wot.network.Fingerprint
|
import org.pgpainless.wot.network.Fingerprint
|
||||||
import org.pgpainless.wot.query.Paths
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate a binding.
|
* Authenticate a binding.
|
||||||
|
@ -36,13 +35,12 @@ interface AuthenticateAPI {
|
||||||
* @param targetAmount the targeted trust amount required to achieve full authentication
|
* @param targetAmount the targeted trust amount required to achieve full authentication
|
||||||
* @param paths the number of paths
|
* @param paths the number of paths
|
||||||
*/
|
*/
|
||||||
data class Result(val fingerprint: Fingerprint, val userId: String, private val targetAmount: Int, val paths: Paths) {
|
data class Result(val binding: Binding, val targetAmount: Int) {
|
||||||
|
|
||||||
/**
|
|
||||||
* Percentage of authentication. 100% means fully authenticated binding.
|
|
||||||
*/
|
|
||||||
val percentage: Int
|
val percentage: Int
|
||||||
get() = paths.amount * 100 / targetAmount
|
get() = binding.percentage(targetAmount)
|
||||||
|
|
||||||
|
val acceptable: Boolean
|
||||||
|
get() = binding.paths.amount >= targetAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
package org.pgpainless.wot.api
|
package org.pgpainless.wot.api
|
||||||
|
|
||||||
import org.pgpainless.wot.network.Fingerprint
|
import org.pgpainless.wot.network.Fingerprint
|
||||||
|
import org.pgpainless.wot.network.Node
|
||||||
import org.pgpainless.wot.query.Paths
|
import org.pgpainless.wot.query.Paths
|
||||||
|
|
||||||
interface IdentifyAPI {
|
interface IdentifyAPI {
|
||||||
|
@ -13,5 +14,10 @@ interface IdentifyAPI {
|
||||||
|
|
||||||
data class Arguments(val fingerprint: Fingerprint)
|
data class Arguments(val fingerprint: Fingerprint)
|
||||||
|
|
||||||
data class Result(val paths: Paths)
|
data class Result(val bindings: List<Binding>, val targetAmount: Int) {
|
||||||
|
val acceptable: Boolean
|
||||||
|
get() = bindings.any {
|
||||||
|
it.paths.amount >= targetAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,9 @@
|
||||||
|
|
||||||
package org.pgpainless.wot.api
|
package org.pgpainless.wot.api
|
||||||
|
|
||||||
import org.pgpainless.wot.query.Paths
|
|
||||||
|
|
||||||
interface ListAPI {
|
interface ListAPI {
|
||||||
|
|
||||||
fun list(): Result
|
fun list(): Result
|
||||||
|
|
||||||
data class Result(val paths: Paths)
|
data class Result(val bindings: List<Binding>, val targetAmount: Int)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,16 @@
|
||||||
|
|
||||||
package org.pgpainless.wot.api
|
package org.pgpainless.wot.api
|
||||||
|
|
||||||
import org.pgpainless.wot.query.Paths
|
|
||||||
|
|
||||||
interface LookupAPI {
|
interface LookupAPI {
|
||||||
|
|
||||||
fun lookup(arguments: Arguments): Result
|
fun lookup(arguments: Arguments): Result
|
||||||
|
|
||||||
data class Arguments(val userId: String, val email: Boolean = false)
|
data class Arguments(val userId: String, val email: Boolean = false)
|
||||||
|
|
||||||
data class Result(val paths: Paths)
|
data class Result(val bindings: List<Binding>, val targetAmount: Int) {
|
||||||
|
val acceptable: Boolean
|
||||||
|
get() = bindings.any {
|
||||||
|
it.paths.amount >= targetAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
package org.pgpainless.wot.api
|
package org.pgpainless.wot.api
|
||||||
|
|
||||||
import org.pgpainless.wot.network.Fingerprint
|
import org.pgpainless.wot.query.Query
|
||||||
import org.pgpainless.wot.network.Network
|
import org.pgpainless.wot.network.*
|
||||||
import org.pgpainless.wot.network.ReferenceTime
|
import org.pgpainless.wot.query.Paths
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Web of Trust API, offering different operations.
|
* 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 certificationNetwork if true, all certifications are treated as delegations with infinite trust depth and no regular expressions
|
||||||
* @param trustAmount minimum trust amount
|
* @param trustAmount minimum trust amount
|
||||||
* @param referenceTime reference time at which the web of trust is evaluated
|
* @param referenceTime reference time at which the web of trust is evaluated
|
||||||
* @param knownNotationRegistry registry of known notations
|
|
||||||
*/
|
*/
|
||||||
class WoTAPI(
|
class WoTAPI(
|
||||||
val network: Network,
|
val network: Network,
|
||||||
val trustRoots: List<Fingerprint>,
|
val trustRoots: Roots,
|
||||||
val gossip: Boolean = false,
|
val gossip: Boolean = false,
|
||||||
val certificationNetwork: Boolean = false,
|
val certificationNetwork: Boolean = false,
|
||||||
val trustAmount: Int = AuthenticationLevel.Fully.amount,
|
val trustAmount: Int = AuthenticationLevel.Fully.amount,
|
||||||
|
@ -32,7 +31,7 @@ class WoTAPI(
|
||||||
* Secondary constructor, taking an [AuthenticationLevel] instead of an [Int].
|
* Secondary constructor, taking an [AuthenticationLevel] instead of an [Int].
|
||||||
*/
|
*/
|
||||||
constructor(network: Network,
|
constructor(network: Network,
|
||||||
trustRoots: List<Fingerprint>,
|
trustRoots: Roots,
|
||||||
gossip: Boolean = false,
|
gossip: Boolean = false,
|
||||||
certificationNetwork: Boolean = false,
|
certificationNetwork: Boolean = false,
|
||||||
trustAmount: AuthenticationLevel = AuthenticationLevel.Fully,
|
trustAmount: AuthenticationLevel = AuthenticationLevel.Fully,
|
||||||
|
@ -40,23 +39,82 @@ class WoTAPI(
|
||||||
this(network,trustRoots, gossip,certificationNetwork, trustAmount.amount, referenceTime)
|
this(network,trustRoots, gossip,certificationNetwork, trustAmount.amount, referenceTime)
|
||||||
|
|
||||||
override fun authenticate(arguments: AuthenticateAPI.Arguments): AuthenticateAPI.Result {
|
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 {
|
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<Binding>()
|
||||||
|
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 {
|
override fun list(): ListAPI.Result {
|
||||||
TODO("Not yet implemented")
|
val bindings = mutableListOf<Binding>()
|
||||||
|
network.nodes.forEach {
|
||||||
|
bindings.addAll(identify(IdentifyAPI.Arguments(it.key)).bindings)
|
||||||
|
}
|
||||||
|
return ListAPI.Result(bindings, trustAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun lookup(arguments: LookupAPI.Arguments): LookupAPI.Result {
|
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<Binding>()
|
||||||
|
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 {
|
override fun path(arguments: PathAPI.Arguments): PathAPI.Result {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Node.mapToMatchingUserIds(userId: String, email: Boolean): List<String> {
|
||||||
|
val list = mutableListOf<String>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue