mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-12-24 11:57:59 +01:00
Work on API structure and first baby steps of formatting the Authenticate output
This commit is contained in:
parent
5d15a18e6b
commit
96cfd71e60
15 changed files with 330 additions and 35 deletions
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue