Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WAL-134 Use sd jwt lib instead of w3c lib #756

Merged
merged 14 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package id.walt.credentials.issuance

import id.walt.credentials.utils.W3CDataMergeUtils
import id.walt.credentials.utils.CredentialDataMergeUtils
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
Expand All @@ -16,7 +16,7 @@ import kotlin.uuid.Uuid

@OptIn(ExperimentalJsExport::class, ExperimentalUuidApi::class)
@JsExport
val dataFunctions = mapOf<String, suspend (call: W3CDataMergeUtils.FunctionCall) -> JsonElement>(
val dataFunctions = mapOf<String, suspend (call: CredentialDataMergeUtils.FunctionCall) -> JsonElement>(
"subjectDid" to { it.fromContext() },
"issuerDid" to { it.fromContext() },

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package id.walt.credentials.issuance

import id.walt.credentials.JwtClaims
import id.walt.credentials.VcClaims
import id.walt.credentials.utils.W3CDataMergeUtils.mergeWithMapping
import id.walt.credentials.utils.CredentialDataMergeUtils.mergeWithMapping
import id.walt.credentials.utils.W3CVcUtils.overwrite
import id.walt.credentials.utils.W3CVcUtils.update
import id.walt.credentials.vc.vcs.W3CVC
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package id.walt.credentials.utils

import id.walt.credentials.vc.vcs.W3CVC
import id.walt.crypto.utils.JsonUtils.toJsonObject
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.serialization.json.*
import love.forte.plugin.suspendtrans.annotation.JsPromise
Expand All @@ -14,7 +15,7 @@ import kotlin.js.JsExport

@OptIn(ExperimentalJsExport::class)
@JsExport
object W3CDataMergeUtils {
object CredentialDataMergeUtils {

private val log = KotlinLogging.logger { }

Expand Down Expand Up @@ -44,7 +45,7 @@ object W3CDataMergeUtils {
try {
func.invoke(FunctionCall(cmd, null, context, null))
} catch (e: NullPointerException) {
e.printStackTrace()
log.error { e }
throw IllegalArgumentException("Could not execute dynamic data function \"$cmd\" - missing argument! At function call: $cmdLine")
}
}
Expand Down Expand Up @@ -77,13 +78,9 @@ object W3CDataMergeUtils {
is JsonObject -> {
v.jsonObject.forEach { (k2, v2) ->
if (!this.containsKey(k)) {
//println("Creating for $k: (to do: $v)")
this[k] = JsonObject(emptyMap())
//println("We now have: $this")
}

//println("Sub-patching for $k: (current is: ${this[k]})")

val kJson = runCatching { this[k]?.jsonObject }.getOrElse { ex ->
throw IllegalArgumentException(
"Invalid mapping for credential, when processing \"$k\": ${ex.message}",
Expand Down Expand Up @@ -113,7 +110,7 @@ object W3CDataMergeUtils {
}

else -> {
println("Unsupported: $v")
log.debug { "Unsupported: $v" }
}
}

Expand All @@ -122,6 +119,7 @@ object W3CDataMergeUtils {


data class MergeResult(val vc: W3CVC, val results: Map<String, JsonElement>)
data class JsonMergeResult(val vc: JsonObject, val results: Map<String, JsonElement>)


data class FunctionCall(
Expand All @@ -131,7 +129,7 @@ object W3CDataMergeUtils {
val args: String?
) {
fun fromContext(): JsonElement {
println("CONTEXT: $context")
log.debug { "CONTEXT: $context" }
return context[func] ?: throw IllegalArgumentException("Cannot find in context: $func")
}
}
Expand All @@ -158,4 +156,27 @@ object W3CDataMergeUtils {
}
return MergeResult(W3CVC(vcm), results)
}

@JvmBlocking
@JvmAsync
@JsPromise
@JsExport.Ignore
suspend fun JsonObject.mergeSDJwtVCPayloadWithMapping(
mapping: JsonObject,
context: Map<String, JsonElement>,
data: Map<String, suspend (FunctionCall) -> JsonElement>
): JsonObject {
val vcm = this.toMutableMap()

val functionHistory = HashMap<String, JsonElement>()

mapping.forEach { (k, v) ->
if (!k.startsWith("jwt:")) {
vcm.patch(k, v, data, context, functionHistory)
} else {
vcm[k.removePrefix("jwt:")] = getTemplateData(v.jsonPrimitive.content, data, context, functionHistory)
}
}
return vcm.toJsonObject()
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package id.walt.oid4vc.data.dif

import id.walt.oid4vc.data.JsonDataObject
import id.walt.oid4vc.data.JsonDataObjectFactory
import id.walt.oid4vc.data.JsonDataObjectSerializer
import id.walt.oid4vc.data.OpenId4VPProfile
import id.walt.oid4vc.data.*
import id.walt.oid4vc.util.ShortIdUtils
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.SerialName
Expand Down Expand Up @@ -40,18 +37,17 @@ data class PresentationDefinition(
override fun fromJSON(jsonObject: JsonObject) =
Json.decodeFromJsonElement(PresentationDefinitionSerializer, jsonObject)

fun primitiveGenerationFromVcTypes(types: List<String>, openId4VPProfile: OpenId4VPProfile = OpenId4VPProfile.DEFAULT): PresentationDefinition {
fun defaultGenerationFromVcTypesForCredentialFormat(types: List<String>, format: CredentialFormat): PresentationDefinition {
return PresentationDefinition(inputDescriptors = types.map { type ->
when(openId4VPProfile) {
OpenId4VPProfile.HAIP -> generateDefaultHAIPInputDescriptor(type)
OpenId4VPProfile.ISO_18013_7_MDOC -> generateDefaultMDOCInputDescriptor(type)
OpenId4VPProfile.EBSIV3 -> generateDefaultEBSIV3InputDescriptor(type)
else -> generateDefaultInputDescriptor(type)
when(format) {
CredentialFormat.sd_jwt_vc -> generateDefaultSDJwtVCInputDescriptor(type)
CredentialFormat.mso_mdoc -> generateDefaultMDOCInputDescriptor(type)
else -> generateDefaultW3CInputDescriptor(type)
}
})
}

private fun generateDefaultInputDescriptor(type: String) = InputDescriptor(
fun generateDefaultW3CInputDescriptor(type: String) = InputDescriptor(
id = type,
format = mapOf(VCFormat.jwt_vc_json to VCFormatDefinition(alg = setOf("EdDSA"))),
constraints = InputDescriptorConstraints(
Expand All @@ -67,7 +63,7 @@ data class PresentationDefinition(
)
)

private fun generateDefaultHAIPInputDescriptor(type: String) = InputDescriptor(
fun generateDefaultSDJwtVCInputDescriptor(type: String) = InputDescriptor(
id = type,
format = mapOf(VCFormat.sd_jwt_vc to VCFormatDefinition()),
constraints = InputDescriptorConstraints(
Expand All @@ -80,7 +76,7 @@ data class PresentationDefinition(
)
)

private fun generateDefaultMDOCInputDescriptor(type: String) = InputDescriptor(
fun generateDefaultMDOCInputDescriptor(type: String) = InputDescriptor(
id = type,
format = mapOf(VCFormat.mso_mdoc to VCFormatDefinition(setOf("EdDSA", "ES256"))),
constraints = InputDescriptorConstraints(
Expand All @@ -99,7 +95,7 @@ data class PresentationDefinition(
)
)

private fun generateDefaultEBSIV3InputDescriptor(type: String) = InputDescriptor(
fun generateDefaultEBSIV3InputDescriptor(type: String) = InputDescriptor(
id = type,
format = mapOf(VCFormat.jwt_vc to VCFormatDefinition(alg = setOf("ES256"))),
constraints = InputDescriptorConstraints(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -940,7 +940,7 @@ class CI_JVM_Test {
} ?: throw IllegalArgumentException("Invalid VC type for requested credential: $it")
}

val presentationDefinition = PresentationDefinition.primitiveGenerationFromVcTypes(requestedTypes, vpProfile)
val presentationDefinition = PresentationDefinition.defaultGenerationFromVcTypesForCredentialFormat(requestedTypes, CredentialFormat.jwt_vc)

// Issuer Client creates state and nonce for the vp_token authorization request
authReqIssuerState = "secured_state_issuer_vptoken"
Expand Down Expand Up @@ -2590,4 +2590,4 @@ suspend fun testIsolatedFunctionsResolveCredentialOffer(credOfferUrl: String): O
println("offeredCredentials[0]: $offeredCredential")

return offeredCredential
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class SDJwtVC(sdJwt: SDJwt): SDJwt(sdJwt.jwt, sdJwt.header, sdJwt.sdPayload, sdJ
audience: String? = null, nonce: String? = null): VCVerificationResult {
var message: String = ""
return VCVerificationResult(
this, verify(jwtCryptoProvider, issuer ?: header["kid"]?.jsonPrimitive?.content),
(notBefore?.let { Clock.System.now().epochSeconds > it } ?: true).also {
this, verify(jwtCryptoProvider, header["kid"]?.jsonPrimitive?.content ?: issuer),
(notBefore?.let { Clock.System.now().epochSeconds >= it } ?: true).also {
if(!it) message = "$message, VC is not valid before $notBefore"
} &&
(expiration?.let { Clock.System.now().epochSeconds < it } ?: true).also {
Expand All @@ -42,7 +42,8 @@ class SDJwtVC(sdJwt: SDJwt): SDJwt(sdJwt.jwt, sdJwt.header, sdJwt.sdPayload, sdJ
} &&
verifyHolderKeyBinding(jwtCryptoProvider, requiresHolderKeyBinding, audience, nonce).also {
if(!it) message = "$message, holder key binding could not be verified"
}
},
message
)
}

Expand Down Expand Up @@ -90,6 +91,22 @@ class SDJwtVC(sdJwt: SDJwt): SDJwt(sdJwt.jwt, sdJwt.header, sdJwt.sdPayload, sdJ
put("jwk", holderKeyJWK)
}, issuerKeyId, vct, nbf, exp, status, additionalJwtHeader, subject)

fun sign(
sdPayload: SDPayload,
jwtCryptoProvider: JWTCryptoProvider,
issuerDid: String,
holderDid: String?,
holderKeyJWK: JsonObject?,
issuerKeyId: String? = null,
vct: String, nbf: Long? = null, exp: Long? = null, status: String? = null,
/** Set additional options in the JWT header */
additionalJwtHeader: Map<String, Any> = emptyMap()
): SDJwtVC = holderDid?.let {
sign(sdPayload, jwtCryptoProvider, issuerDid, it, issuerKeyId, vct, nbf, exp, status, additionalJwtHeader)
} ?: holderKeyJWK?.let {
sign(sdPayload, jwtCryptoProvider, issuerDid, it, issuerKeyId, vct, nbf, exp, status, additionalJwtHeader)
} ?: throw IllegalArgumentException("Either holderKey or holderDid must be given")

private fun doSign(
sdPayload: SDPayload,
jwtCryptoProvider: JWTCryptoProvider,
Expand All @@ -102,7 +119,7 @@ class SDJwtVC(sdJwt: SDJwt): SDJwt(sdJwt.jwt, sdJwt.header, sdJwt.sdPayload, sdJ
subject: String? = null
): SDJwtVC {
val undisclosedPayload = sdPayload.undisclosedPayload.plus(
defaultPayloadProperties(issuerDid, cnf, vct, nbf, exp, status,subject)
defaultPayloadProperties(issuerDid, cnf, vct, nbf , exp, status,subject)
).let { JsonObject(it) }

val finalSdPayload = SDPayload(undisclosedPayload, sdPayload.digestedDisclosures)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import id.walt.crypto.utils.JsonUtils.toJsonElement
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.JsonObject

class WaltIdJWTCryptoProvider(val keys: Map<String, Key>): JWTCryptoProvider {
open class WaltIdJWTCryptoProvider(val keys: Map<String, Key>): JWTCryptoProvider {
constructor(key: Key): this(mapOf(runBlocking{ key.getKeyId() } to key))
override fun sign(payload: JsonObject, keyID: String?, typ: String, headers: Map<String, Any>): String = runBlocking {
val key = keyID?.let { keys[it] } ?: throw Exception("No key found")
if(!key.hasPrivateKey) throw Exception("Key has no private key")
Expand All @@ -28,3 +29,7 @@ class WaltIdJWTCryptoProvider(val keys: Map<String, Key>): JWTCryptoProvider {
}

}

class SingleKeyJWTCryptoProvider(key: Key): WaltIdJWTCryptoProvider(key) {
fun sign(payload: JsonObject, typ: String, headers: Map<String, Any>) = sign(payload, keys.keys.first(), typ, headers)
}
2 changes: 2 additions & 0 deletions waltid-services/waltid-e2e-tests/src/test/kotlin/E2ETest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ class E2ETest {
lspPotentialWallet.testMdocPresentation()
lspPotentialWallet.testSDJwtVCIssuance()
lspPotentialWallet.testSDJwtPresentation(OpenId4VPProfile.HAIP)
lspPotentialWallet.testSDJwtPresentation(OpenId4VPProfile.DEFAULT)
lspPotentialWallet.testSDJwtVCIssuanceByIssuerDid()
lspPotentialWallet.testSDJwtPresentation(OpenId4VPProfile.DEFAULT)

Expand Down Expand Up @@ -345,6 +346,7 @@ class E2ETest {

lspPotentialWallet.testSDJwtVCIssuance()
lspPotentialWallet.testSDJwtPresentation(OpenId4VPProfile.HAIP)
lspPotentialWallet.testSDJwtPresentation(OpenId4VPProfile.DEFAULT)
}

//@Test // enable to execute test selectively
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ class LspPotentialIssuance(val client: HttpClient) {
println("Offered credentials: $offeredCredentials")
val offeredCredential = offeredCredentials.first()
assertEquals(CredentialFormat.sd_jwt_vc, offeredCredential.format)
// assertEquals("identity_credential_vc+sd-jwt", offeredCredential.docType)
assertEquals("http://localhost:22222/identity_credential", offeredCredential.vct)

// ### step 11: confirm issuance (nothing to do)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
@file:OptIn(ExperimentalUuidApi::class)

import id.walt.commons.interop.LspPotentialInterop
import id.walt.credentials.vc.vcs.W3CVC
import id.walt.crypto.keys.KeyGenerationRequest
import id.walt.crypto.keys.KeySerialization
import id.walt.crypto.keys.KeyType
Expand Down Expand Up @@ -129,33 +128,49 @@ class LspPotentialWallet(val client: HttpClient, val walletId: String) {
IssuanceRequest(
Json.parseToJsonElement(KeySerialization.serializeKey(LspPotentialIssuanceInterop.POTENTIAL_ISSUER_JWK_KEY)).jsonObject,
"identity_credential_vc+sd-jwt",
credentialData = W3CVC(buildJsonObject {
credentialData = buildJsonObject {
put("family_name", "Doe")
put("given_name", "John")
put("birthdate", "1940-01-01")
}),
},
"identity_credential",
x5Chain = listOf(LspPotentialInterop.POTENTIAL_ISSUER_CERT),
trustedRootCAs = listOf(LspPotentialInterop.POTENTIAL_ROOT_CA_CERT),
selectiveDisclosure = SDMap(mapOf(
"birthdate" to SDField(sd = true)
))
)),
mapping = Json.parseToJsonElement("""
{
"id": "<uuid>",
"iat": "<timestamp-seconds>",
"nbf": "<timestamp-seconds>",
"exp": "<timestamp-in-seconds:365d>"
}
""".trimIndent()).jsonObject
)
)

suspend fun testSDJwtVCIssuanceByIssuerDid() = testSDJwtVCIssuance(
IssuanceRequest(
Json.parseToJsonElement(KeySerialization.serializeKey(LspPotentialIssuanceInterop.POTENTIAL_ISSUER_JWK_KEY)).jsonObject,
"identity_credential_vc+sd-jwt",
credentialData = W3CVC(buildJsonObject {
credentialData = buildJsonObject {
put("family_name", "Doe")
put("given_name", "John")
put("birthdate", "1940-01-01")
}),
},
mdocData = null,
selectiveDisclosure = SDMap(mapOf(
"birthdate" to SDField(sd = true)
)),
mapping = Json.parseToJsonElement("""
{
"id": "<uuid>",
"iat": "<timestamp-seconds>",
"nbf": "<timestamp-seconds>",
"exp": "<timestamp-in-seconds:365d>"
}
""".trimIndent()).jsonObject,
issuerDid = LspPotentialIssuanceInterop.ISSUER_DID
)
)
Expand Down Expand Up @@ -200,6 +215,15 @@ class LspPotentialWallet(val client: HttpClient, val walletId: String) {
val sdJwtVC = SDJwtVC.parse("${fetchedCredential.document}~${fetchedCredential.disclosures}")
assert(sdJwtVC.disclosures.isNotEmpty())
assert(sdJwtVC.sdMap["birthdate"]!!.sd)
val id = sdJwtVC.undisclosedPayload["id"]?.jsonPrimitive?.contentOrNull ?: ""
val iat = sdJwtVC.undisclosedPayload["iat"]?.jsonPrimitive?.longOrNull ?: 0L
val nbf = sdJwtVC.undisclosedPayload["nbf"]?.jsonPrimitive?.longOrNull ?: 0L
val exp = sdJwtVC.undisclosedPayload["exp"]?.jsonPrimitive?.longOrNull ?: 0L
assert(iat > 0)
assert(iat == nbf)
assert(exp == iat + 365*24*60*60)
assert(id.startsWith("urn:uuid:"))

}

suspend fun testSDJwtPresentation(openIdProfile: OpenId4VPProfile = OpenId4VPProfile.HAIP) = E2ETestWebService.test("test sd-jwt-vc presentation") {
Expand Down
Loading
Loading