1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-12-26 04:47:59 +01:00

Work on API structure and first baby steps of formatting the Authenticate output

This commit is contained in:
Paul Schaub 2023-07-03 20:11:01 +02:00
parent 5d15a18e6b
commit 96cfd71e60
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
15 changed files with 330 additions and 35 deletions

View file

@ -6,9 +6,10 @@ package org.pgpainless.wot.cli
import org.pgpainless.certificate_store.PGPainlessCertD import org.pgpainless.certificate_store.PGPainlessCertD
import org.pgpainless.util.DateUtil import org.pgpainless.util.DateUtil
import org.pgpainless.wot.api.WoTAPI
import org.pgpainless.wot.cli.subcommands.* import org.pgpainless.wot.cli.subcommands.*
import org.pgpainless.wot.dijkstra.sq.Fingerprint
import org.pgpainless.wot.dijkstra.sq.ReferenceTime import org.pgpainless.wot.dijkstra.sq.ReferenceTime
import pgp.cert_d.PGPCertificateDirectory
import pgp.cert_d.PGPCertificateStoreAdapter import pgp.cert_d.PGPCertificateStoreAdapter
import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory
import pgp.certificate_store.PGPCertificateStore import pgp.certificate_store.PGPCertificateStore
@ -65,25 +66,32 @@ class WotCLI: Callable<Int> {
@Option(names = ["--time"], description = ["Reference time."]) @Option(names = ["--time"], description = ["Reference time."])
var time: String? = null var time: String? = null
fun getReferenceTime(): ReferenceTime { private val referenceTime: ReferenceTime
return if (time == null) { get() {
ReferenceTime.now() return if (time == null) {
} else { ReferenceTime.now()
val date = DateUtil.parseUTCDate(time) } else {
ReferenceTime.timestamp(date) val date = DateUtil.parseUTCDate(time)
ReferenceTime.timestamp(date)
}
} }
}
fun getCertificateStore(): PGPCertificateStore { private val certificateStore: PGPCertificateStore
requireNotNull(certificateSource.pgpCertD) { get() {
"Currently, only --cert-d is supported." requireNotNull(certificateSource.pgpCertD) {
"Currently, only --cert-d is supported."
}
val certD = PGPainlessCertD.fileBased(
certificateSource.pgpCertD,
InMemorySubkeyLookupFactory())
return PGPCertificateStoreAdapter(certD)
} }
val certD = PGPainlessCertD.fileBased(
certificateSource.pgpCertD,
InMemorySubkeyLookupFactory())
return PGPCertificateStoreAdapter(certD) private val trustRoots: List<Fingerprint>
} get() {
return trustRoot.map { Fingerprint(it) }
}
/** /**
* Execute the command. * Execute the command.
@ -99,6 +107,17 @@ class WotCLI: Callable<Int> {
return 0 return 0
} }
val api: WoTAPI
get() {
return WoTAPI(
certStores = listOf(certificateStore),
trustRoots = trustRoots,
gossip = false,
certificationNetwork = false,
trustAmount = amount,
referenceTime = referenceTime)
}
companion object { companion object {
@JvmStatic @JvmStatic
fun main(args: Array<String>): Unit = exitProcess(CommandLine(WotCLI()).execute(*args)) fun main(args: Array<String>): Unit = exitProcess(CommandLine(WotCLI()).execute(*args))

View file

@ -6,10 +6,13 @@ package org.pgpainless.wot.cli.subcommands
import org.pgpainless.wot.api.AuthenticateAPI import org.pgpainless.wot.api.AuthenticateAPI
import org.pgpainless.wot.cli.WotCLI import org.pgpainless.wot.cli.WotCLI
import org.pgpainless.wot.dijkstra.sq.Fingerprint
import org.pgpainless.wot.dijkstra.sq.Path
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Command import picocli.CommandLine.Command
import picocli.CommandLine.Parameters import picocli.CommandLine.Parameters
import picocli.CommandLine.ParentCommand import picocli.CommandLine.ParentCommand
import java.text.SimpleDateFormat
import java.util.concurrent.Callable import java.util.concurrent.Callable
@Command(name = "authenticate") @Command(name = "authenticate")
@ -27,12 +30,46 @@ 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
*/ */
override fun call(): Int { override fun call(): Int {
val api = AuthenticateAPI() val result = parent.api.authenticate(AuthenticateAPI.Arguments(
TODO("Not yet implemented") Fingerprint(fingerprint), userId, email))
print(formatResult(result))
if (result.percentage < 100) {
return -1
}
return 0
}
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()
} }
} }

View file

@ -4,7 +4,6 @@
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 picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Command import picocli.CommandLine.Command
@ -26,7 +25,7 @@ class IdentifyCmd: Callable<Int> {
* @return exit code * @return exit code
*/ */
override fun call(): Int { override fun call(): Int {
val api = IdentifyAPI() val api = parent.api
TODO("Not yet implemented") TODO("Not yet implemented")
} }

View file

@ -4,7 +4,6 @@
package org.pgpainless.wot.cli.subcommands package org.pgpainless.wot.cli.subcommands
import org.pgpainless.wot.api.ListAPI
import org.pgpainless.wot.cli.WotCLI import org.pgpainless.wot.cli.WotCLI
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Command import picocli.CommandLine.Command
@ -22,7 +21,7 @@ class ListCmd: Callable<Int> {
* @return exit code * @return exit code
*/ */
override fun call(): Int { override fun call(): Int {
val api = ListAPI() val api = parent.api
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

View file

@ -4,7 +4,6 @@
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
@ -27,7 +26,7 @@ class LookupCmd: Callable<Int> {
* @return exit code * @return exit code
*/ */
override fun call(): Int { override fun call(): Int {
val api = LookupAPI() val api = parent.api
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

View file

@ -4,7 +4,6 @@
package org.pgpainless.wot.cli.subcommands package org.pgpainless.wot.cli.subcommands
import org.pgpainless.wot.api.PathAPI
import org.pgpainless.wot.cli.WotCLI import org.pgpainless.wot.cli.WotCLI
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Command import picocli.CommandLine.Command
@ -29,7 +28,7 @@ class PathCmd: Callable<Int> {
* @return exit code * @return exit code
*/ */
override fun call(): Int { override fun call(): Int {
val api = PathAPI() val api = parent.api
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

View file

@ -0,0 +1,58 @@
package org.pgpainless.wot.cli.subcommands
import org.junit.jupiter.api.Test
import org.pgpainless.wot.api.AuthenticateAPI
import org.pgpainless.wot.dijkstra.sq.*
import java.text.SimpleDateFormat
import kotlin.test.assertEquals
class AuthenticateCmdTest {
@Test
fun testFormatting() {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
val cmd = AuthenticateCmd()
val paths = Paths()
val neal = CertSynopsis(
Fingerprint("F7173B3C7C685CD9ECC4191B74E445BA0E15C957"),
null,
RevocationState.notRevoked(),
mapOf(
Pair("Neal H. Walfield (Code Signing Key) <neal@pep.foundation>", RevocationState.notRevoked())
)
)
val justus = CertSynopsis(
Fingerprint("CBCD8F030588653EEDD7E2659B7DD433F254904A"),
null,
RevocationState.notRevoked(),
mapOf(
Pair("Justus Winter <justus@sequoia-pgp.org>", RevocationState.notRevoked())
)
)
val certification = Certification(
neal,
justus,
"Justus Winter <justus@sequoia-pgp.org>",
dateFormat.parse("2022-02-04"),
null,
true,
120,
Depth.limited(0),
RegexSet.wildcard())
paths.add(Path(neal, mutableListOf(certification), Depth.auto(0)), 120)
val testResult = AuthenticateAPI.Result(
Fingerprint("CBCD8F030588653EEDD7E2659B7DD433F254904A"),
"Justus Winter <justus@sequoia-pgp.org>",
120,
paths)
val formatted = cmd.formatResult(testResult)
assertEquals(buildString {
append("[✓] CBCD8F030588653EEDD7E2659B7DD433F254904A Justus Winter <justus@sequoia-pgp.org>: fully authenticated (100%)\n")
append(" Path #1 of 1, trust amount 120:\n")
append(" ◯ F7173B3C7C685CD9ECC4191B74E445BA0E15C957 (\"Neal H. Walfield (Code Signing Key) <neal@pep.foundation>\")\n")
append(" │ certified the following binding on 2022-02-04\n")
append(" └ CBCD8F030588653EEDD7E2659B7DD433F254904A \"Justus Winter <justus@sequoia-pgp.org>\"\n")
}, formatted)
}
}

View file

@ -4,10 +4,46 @@
package org.pgpainless.wot.api package org.pgpainless.wot.api
import org.pgpainless.wot.dijkstra.sq.Fingerprint
import org.pgpainless.wot.dijkstra.sq.Paths
import org.pgpainless.wot.dijkstra.sq.ReferenceTime
/** /**
* Authenticate a binding. * Authenticate a binding.
* A binding is a pair consisting of a certificate and a User ID. * A binding is a pair consisting of a certificate and a User ID.
*/ */
class AuthenticateAPI { interface AuthenticateAPI {
/**
* Authenticate the binding between a fingerprint and a given userId.
*
* @param arguments arguments
*/
fun authenticate(arguments: Arguments): Result
/**
* Bundle for arguments to the authenticate operation.
* @param fingerprint fingerprint of the certificate
* @param userId user-ID for which we want to authenticate a binding to the certificate
* @param email if true, consider [userId] to be an email address and consider all bindings containing it
*/
data class Arguments(
var fingerprint: Fingerprint,
var userId: String,
var email: Boolean = false)
/**
* Authentication result.
* @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.
*/
val percentage: Int
get() = paths.amount * 100 / targetAmount
}
} }

View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.api
/**
* Enum for different levels of Trust.
*/
enum class AuthenticationLevel(val amount: Int) {
/**
* With an amount of 40, a binding is considered partially trusted.
*/
Partially(40),
/**
* An amount if 120 is sufficient to fully authenticate a binding.
*/
Fully(120),
/**
* A trust amount of 240 means the binding is doubly authenticated.
*/
Doubly(240)
}

View file

@ -4,6 +4,14 @@
package org.pgpainless.wot.api package org.pgpainless.wot.api
class IdentifyAPI { import org.pgpainless.wot.dijkstra.sq.Fingerprint
import org.pgpainless.wot.dijkstra.sq.Paths
interface IdentifyAPI {
fun identify(arguments: Arguments): Result
data class Arguments(val fingerprint: Fingerprint)
data class Result(val paths: Paths)
} }

View file

@ -4,6 +4,11 @@
package org.pgpainless.wot.api package org.pgpainless.wot.api
class ListAPI { import org.pgpainless.wot.dijkstra.sq.Paths
interface ListAPI {
fun list(): Result
data class Result(val paths: Paths)
} }

View file

@ -4,6 +4,13 @@
package org.pgpainless.wot.api package org.pgpainless.wot.api
class LookupAPI { import org.pgpainless.wot.dijkstra.sq.Paths
interface LookupAPI {
fun lookup(arguments: Arguments): Result
data class Arguments(val userId: String, val email: Boolean = false)
data class Result(val paths: Paths)
} }

View file

@ -4,6 +4,28 @@
package org.pgpainless.wot.api package org.pgpainless.wot.api
class PathAPI { import org.pgpainless.wot.dijkstra.sq.Fingerprint
interface PathAPI {
fun path(arguments: Arguments): Result
data class Arguments(val rootFingerprint: Fingerprint, val pathFingerprints: List<Fingerprint>, val userId: String)
interface Result {
fun isSuccess(): Boolean
class Success: Result {
override fun isSuccess(): Boolean {
return true
}
}
data class Failure(val information: List<String>): Result {
override fun isSuccess(): Boolean {
return false
}
}
}
} }

View file

@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.api
import org.pgpainless.certificate_store.PGPainlessCertD
import org.pgpainless.util.NotationRegistry
import org.pgpainless.wot.dijkstra.sq.Fingerprint
import org.pgpainless.wot.dijkstra.sq.ReferenceTime
import pgp.cert_d.PGPCertificateStoreAdapter
import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory
import pgp.certificate_store.PGPCertificateStore
/**
* Web of Trust API, offering different operations.
*
* @param certStores one or more [PGPCertificateStores][PGPCertificateStore] to retrieve certificates from.
* @param trustRoots one or more [Fingerprints][Fingerprint] of trust-roots.
* @param gossip if true, consider all certificates as weakly trusted trust-roots
* @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 certStores: List<PGPCertificateStore> = getDefaultCertStores(),
val trustRoots: List<Fingerprint>,
val gossip: Boolean = false,
val certificationNetwork: Boolean = false,
val trustAmount: Int = AuthenticationLevel.Fully.amount,
val referenceTime: ReferenceTime = ReferenceTime.now(),
val knownNotationRegistry: NotationRegistry = NotationRegistry()
): AuthenticateAPI, IdentifyAPI, ListAPI, LookupAPI, PathAPI {
/**
* Secondary constructor, taking an [AuthenticationLevel] instead of an [Int].
*/
constructor(certStores: List<PGPCertificateStore> = getDefaultCertStores(),
trustRoots: List<Fingerprint>,
gossip: Boolean = false,
certificationNetwork: Boolean = false,
trustAmount: AuthenticationLevel = AuthenticationLevel.Fully,
referenceTime: ReferenceTime = ReferenceTime.now(),
knownNotationRegistry: NotationRegistry = NotationRegistry()):
this(certStores,trustRoots, gossip,certificationNetwork, trustAmount.amount, referenceTime, knownNotationRegistry)
companion object {
@JvmStatic
fun getDefaultCertStores(): List<PGPCertificateStore> {
val certD = PGPainlessCertD.fileBased(InMemorySubkeyLookupFactory())
val asStore = PGPCertificateStoreAdapter(certD)
return listOf(asStore)
}
}
override fun authenticate(arguments: AuthenticateAPI.Arguments): AuthenticateAPI.Result {
TODO("Not yet implemented")
}
override fun identify(arguments: IdentifyAPI.Arguments): IdentifyAPI.Result {
TODO("Not yet implemented")
}
override fun list(): ListAPI.Result {
TODO("Not yet implemented")
}
override fun lookup(arguments: LookupAPI.Arguments): LookupAPI.Result {
TODO("Not yet implemented")
}
override fun path(arguments: PathAPI.Arguments): PathAPI.Result {
TODO("Not yet implemented")
}
}

View file

@ -7,15 +7,20 @@ package org.pgpainless.wot.dijkstra.sq
/** /**
* List of individual [Paths][Path]. * List of individual [Paths][Path].
* *
* @param paths list of paths * @param _paths list of paths
*/ */
class Paths(private val paths: MutableList<Item>) { class Paths(private val _paths: MutableList<Item>) {
/** /**
* Empty collection of paths. * Empty collection of paths.
*/ */
constructor(): this(mutableListOf<Item>()) constructor(): this(mutableListOf<Item>())
val paths: List<Path>
get() {
return _paths.map { it.path }
}
/** /**
* Add a [Path] to the list. * Add a [Path] to the list.
* *
@ -25,7 +30,7 @@ class Paths(private val paths: MutableList<Item>) {
require(amount <= path.amount) { require(amount <= path.amount) {
"Amount too small. TODO: Better error message" "Amount too small. TODO: Better error message"
} }
paths.add(Item(path, amount)) _paths.add(Item(path, amount))
} }
/** /**
@ -33,7 +38,7 @@ class Paths(private val paths: MutableList<Item>) {
*/ */
val amount: Int val amount: Int
get() { get() {
return paths.sumOf { it.amount } return _paths.sumOf { it.amount }
} }
/** /**