Skip to content

Commit

Permalink
Creation of VerifiablePresentations via Custodian REST API, close #62
Browse files Browse the repository at this point in the history
  • Loading branch information
waltkb committed Nov 28, 2021
1 parent 6b0855f commit b19dba5
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 30 deletions.
37 changes: 30 additions & 7 deletions src/main/kotlin/id/walt/cli/VcCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProofType>().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<ProofType>()
.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()

Expand All @@ -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")
Expand Down Expand Up @@ -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<Path, String> = 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)
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/id/walt/rest/custodian/CustodianAPI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand Down
38 changes: 31 additions & 7 deletions src/main/kotlin/id/walt/rest/custodian/CustodianController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<GenerateKeyRequest> { it.description("Generate Key Request") }
.json<String>("200") { it.description("Created key") }

Expand Down Expand Up @@ -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)
Expand All @@ -110,24 +113,29 @@ 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<String>("id", isRepeatable = true)
.json<String>("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(
// summary = "Lists all credential ids the custodian knows of", operationId = "listCredentialIds", tags = ["Credentials"],
// 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<String>("200") { it.description("Credentials ID list") }

fun listCredentialIds(ctx: Context) {
Expand All @@ -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<String>("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<PresentCredentialsRequest>()
.json<VerifiablePresentation>("200") { it.description("The newly created VerifiablePresentation") }


fun presentCredentials(ctx: Context) {
val req = ctx.bodyAsClass<PresentCredentialsRequest>()
ctx.result(custodian.createPresentation(req.vcs, req.holderDid, req.verifierDid, req.domain, req.challenge))
}

}
Original file line number Diff line number Diff line change
@@ -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<String>,
val holderDid: String,
@Json(serializeNull = false) val verifierDid: String? = null,
@Json(serializeNull = false) val domain: String? = null,
@Json(serializeNull = false) val challenge: String? = null
)
94 changes: 78 additions & 16 deletions src/test/kotlin/id/walt/rest/CustodianApiTest.kt
Original file line number Diff line number Diff line change
@@ -1,49 +1,111 @@
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.*
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()
response shouldContain "\"operationId\":\"health\""
response shouldContain "Returns HTTP 200 in case all services are up and running"
}*/
})








0 comments on commit b19dba5

Please sign in to comment.