From f6cfa69ee4d320af8bd5e34c19b553a6ce15bdc2 Mon Sep 17 00:00:00 2001 From: severinstampler Date: Tue, 19 Oct 2021 16:41:53 +0200 Subject: [PATCH 1/4] feat siopv2 request data model, cli data provider for verifiable ID --- .../id/walt/model/siopv2/SIOPv2Request.kt | 96 +++++++++++++++++++ .../id/walt/signatory/CLIDataProvider.kt | 25 +++++ 2 files changed, 121 insertions(+) create mode 100644 src/main/kotlin/id/walt/model/siopv2/SIOPv2Request.kt diff --git a/src/main/kotlin/id/walt/model/siopv2/SIOPv2Request.kt b/src/main/kotlin/id/walt/model/siopv2/SIOPv2Request.kt new file mode 100644 index 00000000..1a493e40 --- /dev/null +++ b/src/main/kotlin/id/walt/model/siopv2/SIOPv2Request.kt @@ -0,0 +1,96 @@ +package id.walt.model.siopv2 + +import com.beust.klaxon.Json +import com.beust.klaxon.Klaxon +import id.walt.model.Claim +import io.javalin.http.Context +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +data class SIOPv2Request( + val response_type: String = "id_token", + val client_id: String, + val redirect_uri: String, + val scope: String = "openid", + val nonce: String, + val registration: Registration = Registration(), + @Json("exp") val expiration: Long, + @Json("iat") val issuedAt: Long, + val claims: Claims + + ) { + private fun enc(str: String): String = URLEncoder.encode(str, StandardCharsets.UTF_8) + fun toUriQueryString(): String { + return "response_type=${enc(response_type)}&client_id=${enc(client_id)}&redirect_uri=${enc(redirect_uri)}" + + "&scope=${enc(scope)}&nonce=${enc(nonce)}®istration=${enc(Klaxon().toJsonString(registration))}" + + "&exp=$expiration&iat=$issuedAt&claims=${enc(Klaxon().toJsonString(claims))}" + } + + companion object { + fun fromHttpContext(ctx: Context): SIOPv2Request { + val requiredParams = setOf("client_id", "redirect_uri", "nonce", "registration", "exp", "iat", "claims") + if (requiredParams.any { ctx.queryParam(it).isNullOrEmpty() }) + throw IllegalArgumentException("HTTP context missing mandatory query parameters") + return SIOPv2Request( + ctx.queryParam("response_type") ?: "id_token", + ctx.queryParam("client_id")!!, + ctx.queryParam("redirect_uri")!!, + ctx.queryParam("scope") ?: "openid", + ctx.queryParam("nonce")!!, + Klaxon().parse(ctx.queryParam("registration")!!)!!, + ctx.queryParam("exp")!!.toLong(), + ctx.queryParam("iat")!!.toLong(), + Klaxon().parse(ctx.queryParam("claims")!!)!! + ) + } + } +} + +data class Registration( + val subject_identifier_types_supported: List = listOf("did"), + val did_methods_supported: List = listOf("did:ebsi:"), + val vp_formats: VPFormats = VPFormats(), + val client_name: String? = null, + val client_purpose: String? = null, + val tos_uri: String? = null, + val logo_uri: String? = null +) + +data class VPFormats( + val jwt_vp: JwtVPFormat? = JwtVPFormat(), + val ldp_vp: LdpVpFormat? = LdpVpFormat() +) + +data class JwtVPFormat ( + val alg: Set = setOf("EdDSA", "ES256K") +) + +data class LdpVpFormat( + val proof_type: Set = setOf("Ed25519Signature2018") +) + +data class InputSchema ( + val uri: String + ) + +data class InputDescriptor ( + val id: String, + val schema: List + ) + +data class PresentationDefinition ( + val id: String, + val input_descriptors: List + ) + +data class VerifiablePresentations ( + val presentation_definition: PresentationDefinition + ) + +data class VpTokenClaim ( + val verifiable_presentations: VerifiablePresentations + ) + +data class Claims ( + val vp_token: VpTokenClaim? = null + ) \ No newline at end of file diff --git a/src/main/kotlin/id/walt/signatory/CLIDataProvider.kt b/src/main/kotlin/id/walt/signatory/CLIDataProvider.kt index 7d98ca9e..37625aaf 100644 --- a/src/main/kotlin/id/walt/signatory/CLIDataProvider.kt +++ b/src/main/kotlin/id/walt/signatory/CLIDataProvider.kt @@ -2,12 +2,14 @@ package id.walt.signatory import id.walt.vclib.model.VerifiableCredential import id.walt.vclib.vclist.VerifiableDiploma +import id.walt.vclib.vclist.VerifiableId import java.util.* object CLIDataProviders { fun getCLIDataProviderFor(templateId: String): SignatoryDataProvider? { return when(templateId) { "VerifiableDiploma" -> VerifiableDiplomaCLIDataProvider() + "VerifiableId" -> VerifiableIDCLIDataProvider() else -> null } } @@ -87,5 +89,28 @@ class VerifiableDiplomaCLIDataProvider : CLIDataProvider() { vc.credentialSubject!!.learningSpecification?.nqfLevel = listOf(prompt("NQF Level", vc.credentialSubject!!.learningSpecification?.nqfLevel?.get(0)) ?: "") return vc } +} + +class VerifiableIDCLIDataProvider : CLIDataProvider() { + override fun populate(vc: VerifiableCredential, proofConfig: ProofConfig): VerifiableCredential { + vc as VerifiableId + vc.id = proofConfig.id ?: "education#higherEducation#${UUID.randomUUID()}" + vc.issuer = proofConfig.issuerDid + if (proofConfig.issueDate != null) vc.issuanceDate = dateFormat.format(proofConfig.issueDate) + if (proofConfig.expirationDate != null) vc.expirationDate = dateFormat.format(proofConfig.expirationDate) + vc.validFrom = vc.issuanceDate + vc.credentialSubject!!.id = proofConfig.subjectDid + println() + println("Subject personal data, ID: ${proofConfig.subjectDid}") + println("----------------------") + vc.credentialSubject!!.firstName = prompt("First name", vc.credentialSubject!!.firstName) + vc.credentialSubject!!.familyName = prompt("Family name", vc.credentialSubject!!.familyName) + vc.credentialSubject!!.dateOfBirth = prompt("Date of birth", vc.credentialSubject!!.dateOfBirth) + vc.credentialSubject!!.gender = prompt("Gender", vc.credentialSubject!!.gender) + vc.credentialSubject!!.placeOfBirth = prompt("Place of birth", vc.credentialSubject!!.placeOfBirth) + vc.credentialSubject!!.currentAddress = prompt("Current address", vc.credentialSubject!!.currentAddress) + + return vc + } } \ No newline at end of file From 958eaa9cd7b46926a205d7ad6c2d7e2edde00177 Mon Sep 17 00:00:00 2001 From: severinstampler Date: Thu, 21 Oct 2021 15:02:25 +0200 Subject: [PATCH 2/4] adapt siopv2 request, impl siopv2 responses according to: https://openid.net/wordpress-content/uploads/2021/09/OIDF_OIDC4SSI-Update_Kristina-Yasuda-Torsten-Lodderstedt.pdf --- .../id/walt/model/siopv2/SIOPv2Request.kt | 12 +--- .../id/walt/model/siopv2/SIOPv2Response.kt | 59 +++++++++++++++++++ .../kotlin/id/walt/services/did/DidService.kt | 3 +- 3 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/id/walt/model/siopv2/SIOPv2Response.kt diff --git a/src/main/kotlin/id/walt/model/siopv2/SIOPv2Request.kt b/src/main/kotlin/id/walt/model/siopv2/SIOPv2Request.kt index 1a493e40..4f59949b 100644 --- a/src/main/kotlin/id/walt/model/siopv2/SIOPv2Request.kt +++ b/src/main/kotlin/id/walt/model/siopv2/SIOPv2Request.kt @@ -69,13 +69,9 @@ data class LdpVpFormat( val proof_type: Set = setOf("Ed25519Signature2018") ) -data class InputSchema ( - val uri: String - ) - data class InputDescriptor ( val id: String, - val schema: List + val schema: String ) data class PresentationDefinition ( @@ -83,12 +79,8 @@ data class PresentationDefinition ( val input_descriptors: List ) -data class VerifiablePresentations ( - val presentation_definition: PresentationDefinition - ) - data class VpTokenClaim ( - val verifiable_presentations: VerifiablePresentations + val presentation_definition: PresentationDefinition ) data class Claims ( diff --git a/src/main/kotlin/id/walt/model/siopv2/SIOPv2Response.kt b/src/main/kotlin/id/walt/model/siopv2/SIOPv2Response.kt new file mode 100644 index 00000000..0e6332f3 --- /dev/null +++ b/src/main/kotlin/id/walt/model/siopv2/SIOPv2Response.kt @@ -0,0 +1,59 @@ +package id.walt.model.siopv2 + +import com.beust.klaxon.Json +import com.beust.klaxon.Klaxon +import id.walt.services.did.DidService +import id.walt.services.jwt.JwtService +import id.walt.services.vc.VcUtils +import id.walt.vclib.Helpers.encode +import id.walt.vclib.VcLibManager +import id.walt.vclib.vclist.VerifiablePresentation +import java.time.Instant +import java.time.temporal.Temporal +import java.util.* + +data class SIOPv2Response ( + val did: String, + val id_token: SIOPv2IDToken, + val vp_token: SIOPv2VPToken + ) { + init { + DidService.importKey(did) + } + fun getIdToken(): String { + return JwtService.getService().sign(did, Klaxon().toJsonString(id_token)) + } + + fun getVpToken(): String { + return JwtService.getService().sign(did, Klaxon().toJsonString(vp_token)) + } +} + +data class SIOPv2IDToken( + @Json("iss") val issuer: String = "https://self-issued.me/v2", + @Json("sub") val subject: String, + @Json("aud") val client_id: String, + @Json("exp") val expiration: Long = Instant.now().plusSeconds(60*60).epochSecond, + @Json("iat") val issueDate: Long = Instant.now().epochSecond, + val nonce: String) + +data class SIOPv2VPToken( + val vp_token: List +) + +data class SIOPv2Presentation( + val format: String, + val presentation: String +) { + companion object { + fun createFromVPString(vpStr: String): SIOPv2Presentation { + return SIOPv2Presentation( + format = when(VcLibManager.isJWT(vpStr)) { + true -> "jwt_vp" + else -> "ldp_vp" + }, + presentation = vpStr + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/id/walt/services/did/DidService.kt b/src/main/kotlin/id/walt/services/did/DidService.kt index 1a3808d2..6ad858da 100644 --- a/src/main/kotlin/id/walt/services/did/DidService.kt +++ b/src/main/kotlin/id/walt/services/did/DidService.kt @@ -348,7 +348,8 @@ object DidService { KeyService.getService().delete(id!!) pubKeyJwk!!.kid = id WaltIdServices.log.debug { "Importing key: ${pubKeyJwk.kid}" } - KeyService.getService().import(Klaxon().toJsonString(pubKeyJwk)) + val keyId = KeyService.getService().import(Klaxon().toJsonString(pubKeyJwk)) + WaltContext.keyStore.addAlias(keyId, didStr) return true } else -> { From 2a48c54c734d947d194a1d776384979c9fe09fc4 Mon Sep 17 00:00:00 2001 From: severinstampler Date: Fri, 22 Oct 2021 14:47:54 +0200 Subject: [PATCH 3/4] fix siop response signing --- src/main/kotlin/id/walt/model/siopv2/SIOPv2Response.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/kotlin/id/walt/model/siopv2/SIOPv2Response.kt b/src/main/kotlin/id/walt/model/siopv2/SIOPv2Response.kt index 0e6332f3..ec650ca6 100644 --- a/src/main/kotlin/id/walt/model/siopv2/SIOPv2Response.kt +++ b/src/main/kotlin/id/walt/model/siopv2/SIOPv2Response.kt @@ -17,9 +17,6 @@ data class SIOPv2Response ( val id_token: SIOPv2IDToken, val vp_token: SIOPv2VPToken ) { - init { - DidService.importKey(did) - } fun getIdToken(): String { return JwtService.getService().sign(did, Klaxon().toJsonString(id_token)) } From 39e6fde531f41e2490bbbdcb8f7c2550439f0b98 Mon Sep 17 00:00:00 2001 From: severinstampler Date: Fri, 22 Oct 2021 16:57:07 +0200 Subject: [PATCH 4/4] support listing credentials by id --- .../kotlin/id/walt/rest/custodian/CustodianController.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/id/walt/rest/custodian/CustodianController.kt b/src/main/kotlin/id/walt/rest/custodian/CustodianController.kt index 7fb3028d..b9cb0d34 100644 --- a/src/main/kotlin/id/walt/rest/custodian/CustodianController.kt +++ b/src/main/kotlin/id/walt/rest/custodian/CustodianController.kt @@ -111,10 +111,15 @@ object CustodianController { // ) fun listCredentialsDocs() = document() .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) { - ctx.json(ListCredentialsResponse(custodian.listCredentials())) + val ids = ctx.queryParams("id").toSet() + if(ids.isEmpty()) + ctx.json(ListCredentialsResponse(custodian.listCredentials())) + else + ctx.json(ListCredentialsResponse(custodian.listCredentials().filter { it.id != null && ids.contains(it.id!!)})) } // @OpenApi(