From b19dba572f47380454b28911e5c7ca06b8f62325 Mon Sep 17 00:00:00 2001 From: Kevin Burgmann Date: Sun, 28 Nov 2021 02:10:55 +0100 Subject: [PATCH] Creation of VerifiablePresentations via Custodian REST API, close #62 --- src/main/kotlin/id/walt/cli/VcCommand.kt | 37 ++++++-- .../id/walt/rest/custodian/CustodianAPI.kt | 1 + .../rest/custodian/CustodianController.kt | 38 ++++++-- .../custodian/PresentCredentialsRequest.kt | 13 +++ .../kotlin/id/walt/rest/CustodianApiTest.kt | 94 +++++++++++++++---- 5 files changed, 153 insertions(+), 30 deletions(-) create mode 100644 src/main/kotlin/id/walt/rest/custodian/PresentCredentialsRequest.kt diff --git a/src/main/kotlin/id/walt/cli/VcCommand.kt b/src/main/kotlin/id/walt/cli/VcCommand.kt index a94e9622..d826ecc5 100644 --- a/src/main/kotlin/id/walt/cli/VcCommand.kt +++ b/src/main/kotlin/id/walt/cli/VcCommand.kt @@ -54,10 +54,22 @@ class VcIssueCommand : CliktCommand( val template: String by option("-t", "--template", help = "VC template [VerifiableDiploma]").default("VerifiableDiploma") val issuerDid: String by option("-i", "--issuer-did", help = "DID of the issuer (associated with signing key)").required() val subjectDid: String by option("-s", "--subject-did", help = "DID of the VC subject (receiver of VC)").required() - val issuerVerificationMethod: String? by option("-v", "--issuer-verification-method", help = "KeyId of the issuers' signing key") - val proofType: ProofType by option("-y", "--proof-type", help = "Proof type to be used [LD_PROOF]").enum().default(ProofType.LD_PROOF) - val proofPurpose: String by option("-p", "--proof-purpose", help = "Proof purpose to be used [assertion]").default("assertion") - val interactive: Boolean by option("--interactive", help = "Interactively prompt for VC data to fill in").flag(default = false) + val issuerVerificationMethod: String? by option( + "-v", + "--issuer-verification-method", + help = "KeyId of the issuers' signing key" + ) + val proofType: ProofType by option("-y", "--proof-type", help = "Proof type to be used [LD_PROOF]").enum() + .default(ProofType.LD_PROOF) + val proofPurpose: String by option( + "-p", + "--proof-purpose", + help = "Proof purpose to be used [assertion]" + ).default("assertion") + val interactive: Boolean by option( + "--interactive", + help = "Interactively prompt for VC data to fill in" + ).flag(default = false) private val signatory = Signatory.getService() @@ -78,7 +90,13 @@ class VcIssueCommand : CliktCommand( val vcStr = signatory.issue( template, - ProofConfig(issuerDid = issuerDid, subjectDid = subjectDid, issuerVerificationMethod = issuerVerificationMethod, proofType = proofType, proofPurpose = proofPurpose) + ProofConfig( + issuerDid = issuerDid, + subjectDid = subjectDid, + issuerVerificationMethod = issuerVerificationMethod, + proofType = proofType, + proofPurpose = proofPurpose + ) ) echo("\nResults:\n") @@ -129,9 +147,14 @@ class PresentVcCommand : CliktCommand( override fun run() { echo("Creating a verifiable presentation for DID \"$holderDid\"...") echo("Using ${src.size} ${if (src.size > 1) "VCs" else "VC"}:") - src.forEachIndexed { index, vc -> echo("- ${index + 1}. $vc (${vc.readText().toCredential().type.last()})") } - val vcStrList = src.stream().map { vc -> vc.readText() }.collect(Collectors.toList()) + val vcSources: Map = src.associateWith { it.readText() } + + src.forEachIndexed { index, vcPath -> + echo("- ${index + 1}. $vcPath (${vcSources[vcPath]!!.toCredential().type.last()})") + } + + val vcStrList = vcSources.values.toList() // Creating the Verifiable Presentation val vp = Custodian.getService().createPresentation(vcStrList, holderDid, verifierDid, domain, challenge) diff --git a/src/main/kotlin/id/walt/rest/custodian/CustodianAPI.kt b/src/main/kotlin/id/walt/rest/custodian/CustodianAPI.kt index 5d4ae1d3..6936901e 100644 --- a/src/main/kotlin/id/walt/rest/custodian/CustodianAPI.kt +++ b/src/main/kotlin/id/walt/rest/custodian/CustodianAPI.kt @@ -122,6 +122,7 @@ object CustodianAPI { get("listCredentialIds", documented(CustodianController.listCredentialIdsDocs(), CustodianController::listCredentialIds)) put("{alias}", documented(CustodianController.storeCredenitalsDocs(),CustodianController::storeCredential)) delete("{alias}", documented(CustodianController.deleteCredentialDocs(), CustodianController::deleteCredential)) + post("present", documented(CustodianController.presentCredentialsDocs(), CustodianController::presentCredentials)) } }.exception(IllegalArgumentException::class.java) { e, ctx -> log.error { e.stackTraceToString() } diff --git a/src/main/kotlin/id/walt/rest/custodian/CustodianController.kt b/src/main/kotlin/id/walt/rest/custodian/CustodianController.kt index 607753ba..40875e36 100644 --- a/src/main/kotlin/id/walt/rest/custodian/CustodianController.kt +++ b/src/main/kotlin/id/walt/rest/custodian/CustodianController.kt @@ -3,6 +3,7 @@ package id.walt.rest.custodian import id.walt.crypto.Key import id.walt.crypto.KeyAlgorithm import id.walt.custodian.Custodian +import id.walt.vclib.credentials.VerifiablePresentation import id.walt.vclib.model.VerifiableCredential import io.javalin.http.Context import io.javalin.plugin.openapi.dsl.document @@ -24,7 +25,9 @@ object CustodianController { // responses = [OpenApiResponse("200", [OpenApiContent(Key::class)], "Created Key")] // ) fun generateKeyDocs() = document() - .operation { it.summary("Generates a key with a specific key algorithm").operationId("generateKey").addTagsItem("Keys") } + .operation { + it.summary("Generates a key with a specific key algorithm").operationId("generateKey").addTagsItem("Keys") + } .body { it.description("Generate Key Request") } .json("200") { it.description("Created key") } @@ -99,7 +102,7 @@ object CustodianController { fun getCredential(ctx: Context) { val vc = custodian.getCredential(ctx.pathParam("id")) - if(vc == null) + if (vc == null) ctx.status(404).result("Not found") else ctx.json(vc) @@ -110,16 +113,18 @@ object CustodianController { // responses = [OpenApiResponse("200", [OpenApiContent(ListCredentialsResponse::class)], "Credential list")] // ) fun listCredentialsDocs() = document() - .operation { it.summary("Lists all credentials the custodian knows of").operationId("listCredentials").addTagsItem("Credentials") } + .operation { + it.summary("Lists all credentials the custodian knows of").operationId("listCredentials").addTagsItem("Credentials") + } .queryParam("id", isRepeatable = true) .json("200") { it.description("Credentials list") } fun listCredentials(ctx: Context) { val ids = ctx.queryParams("id").toSet() - if(ids.isEmpty()) + if (ids.isEmpty()) ctx.json(ListCredentialsResponse(custodian.listCredentials())) else - ctx.json(ListCredentialsResponse(custodian.listCredentials().filter { it.id != null && ids.contains(it.id!!)})) + ctx.json(ListCredentialsResponse(custodian.listCredentials().filter { it.id != null && ids.contains(it.id!!) })) } // @OpenApi( @@ -127,7 +132,10 @@ object CustodianController { // responses = [OpenApiResponse("200", [OpenApiContent(ListCredentialIdsResponse::class)], "Credential id list")] // ) fun listCredentialIdsDocs() = document() - .operation { it.summary("Lists all credential IDs the custodian knows of").operationId("listCredentialIds").addTagsItem("Credentials") } + .operation { + it.summary("Lists all credential IDs the custodian knows of").operationId("listCredentialIds") + .addTagsItem("Credentials") + } .json("200") { it.description("Credentials ID list") } fun listCredentialIds(ctx: Context) { @@ -153,11 +161,27 @@ object CustodianController { // tags = ["Credentials"], responses = [OpenApiResponse("200")] // ) fun deleteCredentialDocs() = document() - .operation { it.summary("Deletes a specific credential by alias").operationId("deleteCredential").addTagsItem("Credentials") } + .operation { + it.summary("Deletes a specific credential by alias").operationId("deleteCredential").addTagsItem("Credentials") + } .json("200") { it.description("Http OK") } fun deleteCredential(ctx: Context) { custodian.deleteCredential(ctx.pathParam("alias")) } + fun presentCredentialsDocs() = document() + .operation { + it.summary("Create a VerifiablePresentation from specific credentials)").operationId("presentCredentials") + .addTagsItem("Credentials") + } + .body() + .json("200") { it.description("The newly created VerifiablePresentation") } + + + fun presentCredentials(ctx: Context) { + val req = ctx.bodyAsClass() + ctx.result(custodian.createPresentation(req.vcs, req.holderDid, req.verifierDid, req.domain, req.challenge)) + } + } diff --git a/src/main/kotlin/id/walt/rest/custodian/PresentCredentialsRequest.kt b/src/main/kotlin/id/walt/rest/custodian/PresentCredentialsRequest.kt new file mode 100644 index 00000000..d3f7978e --- /dev/null +++ b/src/main/kotlin/id/walt/rest/custodian/PresentCredentialsRequest.kt @@ -0,0 +1,13 @@ +package id.walt.rest.custodian + +import com.beust.klaxon.Json +import kotlinx.serialization.Serializable + +@Serializable +data class PresentCredentialsRequest( + val vcs: List, + val holderDid: String, + @Json(serializeNull = false) val verifierDid: String? = null, + @Json(serializeNull = false) val domain: String? = null, + @Json(serializeNull = false) val challenge: String? = null +) diff --git a/src/test/kotlin/id/walt/rest/CustodianApiTest.kt b/src/test/kotlin/id/walt/rest/CustodianApiTest.kt index 244abd61..f02722b0 100644 --- a/src/test/kotlin/id/walt/rest/CustodianApiTest.kt +++ b/src/test/kotlin/id/walt/rest/CustodianApiTest.kt @@ -1,6 +1,17 @@ package id.walt.rest +import id.walt.auditor.Auditor +import id.walt.auditor.SignaturePolicy +import id.walt.model.DidMethod import id.walt.rest.custodian.CustodianAPI +import id.walt.rest.custodian.PresentCredentialsRequest +import id.walt.servicematrix.ServiceMatrix +import id.walt.services.did.DidService +import id.walt.signatory.ProofConfig +import id.walt.signatory.ProofType +import id.walt.signatory.Signatory +import id.walt.vclib.Helpers.toCredential +import id.walt.vclib.credentials.VerifiablePresentation import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.ktor.client.* @@ -8,38 +19,81 @@ import io.ktor.client.engine.cio.* import io.ktor.client.features.json.* import io.ktor.client.features.json.serializer.* import io.ktor.client.request.* -import io.ktor.client.statement.* import io.ktor.http.* -import kotlinx.coroutines.runBlocking class CustodianApiTest : StringSpec({ - val CUSTODIAN_API_URL = "http://localhost:7013" + ServiceMatrix("service-matrix.properties") val client = HttpClient(CIO) { install(JsonFeature) { - serializer = KotlinxSerializer() + serializer = KotlinxSerializer(kotlinx.serialization.json.Json { encodeDefaults = false }) } - expectSuccess = false } println("${CustodianAPI.DEFAULT_BIND_ADDRESS}/${CustodianAPI.DEFAULT_Custodian_API_PORT}") - fun get(path: String): HttpResponse = runBlocking { - val response: HttpResponse = - client.get("http://${CustodianAPI.DEFAULT_BIND_ADDRESS}:${CustodianAPI.DEFAULT_Custodian_API_PORT}$path") { - headers { - append(HttpHeaders.Accept, "text/html") - append(HttpHeaders.Authorization, "token") - } - } - response.status.value shouldBe 200 - return@runBlocking response - } "Starting Custodian API" { CustodianAPI.start() } + + "Check Custodian Presentation generation LD_PROOF" { + val did = DidService.create(DidMethod.key) + + // Issuance is Signatory stuff, we're just testing the Custodian here + val vcJwt = Signatory.getService().issue( + "VerifiableDiploma", + ProofConfig( + issuerDid = did, + subjectDid = did, + issuerVerificationMethod = "Ed25519Signature2018", + proofType = ProofType.LD_PROOF + ) + ) + + val response: String = + client.post("http://${CustodianAPI.DEFAULT_BIND_ADDRESS}:${CustodianAPI.DEFAULT_Custodian_API_PORT}/credentials/present") { + contentType(ContentType.Application.Json) + body = PresentCredentialsRequest(listOf(vcJwt), did) + } + + val vp = response.toCredential() as VerifiablePresentation + + vp.type shouldBe VerifiablePresentation.type + + println("VP Response: $response") + + Auditor.getService().verify(response, listOf(SignaturePolicy())).valid shouldBe true + } + + "Check Custodian Presentation generation JWT" { + val did = DidService.create(DidMethod.key) + + // Issuance is Signatory stuff, we're just testing the Custodian here + val vcJwt = Signatory.getService().issue( + "VerifiableDiploma", + ProofConfig( + issuerDid = did, + subjectDid = did, + issuerVerificationMethod = "Ed25519Signature2018", + proofType = ProofType.JWT + ) + ) + + val response: String = + client.post("http://${CustodianAPI.DEFAULT_BIND_ADDRESS}:${CustodianAPI.DEFAULT_Custodian_API_PORT}/credentials/present") { + contentType(ContentType.Application.Json) + body = PresentCredentialsRequest(listOf(vcJwt), did) + } + + response.count { it == '.' } shouldBe 2 + + println("VP Response: $response") + + Auditor.getService().verify(response, listOf(SignaturePolicy())).valid shouldBe true + } + /*"Test documentation" { val response = get("/v1/api-documentation").readText() @@ -47,3 +101,11 @@ class CustodianApiTest : StringSpec({ response shouldContain "Returns HTTP 200 in case all services are up and running" }*/ }) + + + + + + + +