1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-11-15 17:02:06 +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.util.DateUtil
import org.pgpainless.wot.api.WoTAPI
import org.pgpainless.wot.cli.subcommands.*
import org.pgpainless.wot.dijkstra.sq.Fingerprint
import org.pgpainless.wot.dijkstra.sq.ReferenceTime
import pgp.cert_d.PGPCertificateDirectory
import pgp.cert_d.PGPCertificateStoreAdapter
import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory
import pgp.certificate_store.PGPCertificateStore
@ -65,25 +66,32 @@ class WotCLI: Callable<Int> {
@Option(names = ["--time"], description = ["Reference time."])
var time: String? = null
fun getReferenceTime(): ReferenceTime {
return if (time == null) {
ReferenceTime.now()
} else {
val date = DateUtil.parseUTCDate(time)
ReferenceTime.timestamp(date)
private val referenceTime: ReferenceTime
get() {
return if (time == null) {
ReferenceTime.now()
} else {
val date = DateUtil.parseUTCDate(time)
ReferenceTime.timestamp(date)
}
}
}
fun getCertificateStore(): PGPCertificateStore {
requireNotNull(certificateSource.pgpCertD) {
"Currently, only --cert-d is supported."
private val certificateStore: PGPCertificateStore
get() {
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.
@ -99,6 +107,17 @@ class WotCLI: Callable<Int> {
return 0
}
val api: WoTAPI
get() {
return WoTAPI(
certStores = listOf(certificateStore),
trustRoots = trustRoots,
gossip = false,
certificationNetwork = false,
trustAmount = amount,
referenceTime = referenceTime)
}
companion object {
@JvmStatic
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.cli.WotCLI
import org.pgpainless.wot.dijkstra.sq.Fingerprint
import org.pgpainless.wot.dijkstra.sq.Path
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Parameters
import picocli.CommandLine.ParentCommand
import java.text.SimpleDateFormat
import java.util.concurrent.Callable
@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."])
var email = false
private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd")
/**
* Execute the command.
* @return exit code
*/
override fun call(): Int {
val api = AuthenticateAPI()
TODO("Not yet implemented")
val result = parent.api.authenticate(AuthenticateAPI.Arguments(
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
import org.pgpainless.wot.api.IdentifyAPI
import org.pgpainless.wot.cli.WotCLI
import picocli.CommandLine
import picocli.CommandLine.Command
@ -26,7 +25,7 @@ class IdentifyCmd: Callable<Int> {
* @return exit code
*/
override fun call(): Int {
val api = IdentifyAPI()
val api = parent.api
TODO("Not yet implemented")
}

View file

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

View file

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

View file

@ -4,7 +4,6 @@
package org.pgpainless.wot.cli.subcommands
import org.pgpainless.wot.api.PathAPI
import org.pgpainless.wot.cli.WotCLI
import picocli.CommandLine
import picocli.CommandLine.Command
@ -29,7 +28,7 @@ class PathCmd: Callable<Int> {
* @return exit code
*/
override fun call(): Int {
val api = PathAPI()
val api = parent.api
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
import org.pgpainless.wot.dijkstra.sq.Fingerprint
import org.pgpainless.wot.dijkstra.sq.Paths
import org.pgpainless.wot.dijkstra.sq.ReferenceTime
/**
* Authenticate a binding.
* 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
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
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
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
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].
*
* @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.
*/
constructor(): this(mutableListOf<Item>())
val paths: List<Path>
get() {
return _paths.map { it.path }
}
/**
* Add a [Path] to the list.
*
@ -25,7 +30,7 @@ class Paths(private val paths: MutableList<Item>) {
require(amount <= path.amount) {
"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
get() {
return paths.sumOf { it.amount }
return _paths.sumOf { it.amount }
}
/**