Skip to content

Commit

Permalink
fix selectively disclosable fields as given in issuance request
Browse files Browse the repository at this point in the history
fix issuance of bogus/demo credential if no holder did is supplied in issuance request
fix default values in sample data, that were reflected in final credential
fix adding additionally specified headers and payload properties to issued sd-jwt credentials
refactor adding default meta properties to sd-jwt vcs
  • Loading branch information
severinstampler committed Aug 5, 2024
1 parent 3a4bace commit 8ba2240
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,24 @@ class SDJwtVC(sdJwt: SDJwt): SDJwt(sdJwt.jwt, sdJwt.header, sdJwt.sdPayload, sdJ
/** Set additional options in the JWT header */
additionalJwtHeader: Map<String, Any> = emptyMap()
): SDJwtVC {
val undisclosedPayload = sdPayload.undisclosedPayload.toMutableMap().apply {
put("iss", JsonPrimitive(issuerDid))
put("cnf", cnf)
put("vct", JsonPrimitive(vct))
nbf?.let { put("nbf", JsonPrimitive(it)) }
exp?.let { put("exp", JsonPrimitive(it)) }
status?.let { put("status", JsonPrimitive(it)) }
}.let { JsonObject(it) }
val undisclosedPayload = sdPayload.undisclosedPayload.plus(
defaultPayloadProperties(issuerDid, cnf, vct, nbf, exp, status)
).let { JsonObject(it) }

val finalSdPayload = SDPayload(undisclosedPayload, sdPayload.digestedDisclosures)
return SDJwtVC(sign(finalSdPayload, jwtCryptoProvider, issuerKeyId, typ = "vc+sd-jwt", additionalJwtHeader))
}

fun defaultPayloadProperties(issuerId: String, cnf: JsonObject, vct: String,
notBefore: Long? = null, expirationDate: Long? = null, status: String? = null) = buildJsonObject {
put("iss", JsonPrimitive(issuerId))
put("cnf", cnf)
put("vct", JsonPrimitive(vct))
notBefore?.let { put("nbf", JsonPrimitive(it)) }
expirationDate?.let { put("exp", JsonPrimitive(it)) }
status?.let { put("status", JsonPrimitive(it)) }
}

fun isSdJwtVCPresentation(token: String): Boolean = parse(token).isPresentation
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ object Issuer {

dataOverwrites: Map<String, JsonElement>,
dataUpdates: Map<String, Map<String, JsonElement>>,
additionalJwtHeader: Map<String, JsonElement>,
additionalJwtHeaders: Map<String, JsonElement>,
additionalJwtOptions: Map<String, JsonElement>
): String {
val overwritten = overwrite(dataOverwrites)
Expand All @@ -49,7 +49,7 @@ object Issuer {
issuerKey = key,
issuerDid = did,
subjectDid = subject,
additionalJwtHeader = additionalJwtHeader,
additionalJwtHeader = additionalJwtHeaders,
additionalJwtOptions = additionalJwtOptions
)
}
Expand Down Expand Up @@ -101,7 +101,7 @@ object Issuer {

mappings: JsonObject,

additionalJwtHeader: Map<String, String>,
additionalJwtHeaders: Map<String, JsonElement>,
additionalJwtOptions: Map<String, JsonElement>,

completeJwtWithDefaultCredentialData: Boolean = true,
Expand All @@ -117,8 +117,8 @@ object Issuer {
issuerDid = issuerDid,
subjectDid = subjectDid,
disclosureMap = disclosureMap,
additionalJwtHeader = additionalJwtHeader.toMutableMap().apply {
put("typ", "JWT")
additionalJwtHeaders = additionalJwtHeaders.toMutableMap().apply {
put("typ", "JWT".toJsonElement())
},
additionalJwtOptions = additionalJwtOptions.toMutableMap().apply {
putAll(jwtOptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ data class W3CVC(
) : Map<String, JsonElement> by content {


fun toJsonObject(): JsonObject = JsonObject(content)
fun toJsonObject(additionalProperties: Map<String, JsonElement> = emptyMap()): JsonObject
= JsonObject(content.plus(additionalProperties))
fun toJson(): String = Json.encodeToString(content)
fun toPrettyJson(): String = prettyJson.encodeToString(content)

Expand All @@ -55,11 +56,11 @@ data class W3CVC(
subjectDid: String,
disclosureMap: SDMap,
/** Set additional options in the JWT header */
additionalJwtHeader: Map<String, String> = emptyMap(),
additionalJwtHeaders: Map<String, JsonElement> = emptyMap(),
/** Set additional options in the JWT payload */
additionalJwtOptions: Map<String, JsonElement> = emptyMap()
): String {
val vc = this.toJsonObject()
val vc = this.toJsonObject(additionalJwtOptions)

val sdPayload = SDPayload.createSDPayload(vc, disclosureMap)
val signable = Json.encodeToString(sdPayload.undisclosedPayload).toByteArray()
Expand All @@ -69,7 +70,7 @@ data class W3CVC(
"typ" to "vc+sd-jwt".toJsonElement(),
"cty" to "credential-claims-set+json".toJsonElement(),
"kid" to issuerDid.toJsonElement()
)
).plus(additionalJwtHeaders)
)

return SDJwt.createFromSignedJwt(signed, sdPayload).toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import kotlin.test.*
class LspPotentialIssuance(val client: HttpClient) {

@OptIn(ExperimentalEncodingApi::class, ExperimentalSerializationApi::class)
fun testTrack1() = runBlocking {
suspend fun testTrack1() = E2ETestWebService.test("test track 1") {
// ### steps 1-6
val offerResp = client.get("/lsp-potential/lspPotentialCredentialOfferT1")
assert(offerResp.status == HttpStatusCode.OK)
Expand Down Expand Up @@ -228,7 +228,7 @@ class LspPotentialIssuance(val client: HttpClient) {
}

@OptIn(ExperimentalEncodingApi::class)
fun testTrack2() = runBlocking {
suspend fun testTrack2() = E2ETestWebService.test("test track 1") {
// ### steps 1-6
val offerResp = client.get("/lsp-potential/lspPotentialCredentialOfferT2")
assertEquals(HttpStatusCode.OK, offerResp.status)
Expand Down Expand Up @@ -337,5 +337,11 @@ class LspPotentialIssuance(val client: HttpClient) {
assertNotNull(credResp.credential)
val sdJwtVc = SDJwtVC.parse(credResp.credential!!.jsonPrimitive.content)
assertNotNull(sdJwtVc.cnfObject)
// family_name is defined as non-selective disclosable in issuance request
assertContains(sdJwtVc.undisclosedPayload.keys, "family_name")
// birthdate is defined as selective disclosable in issuance request
assertFalse(sdJwtVc.undisclosedPayload.keys.contains("birthdate"))
assertContains(sdJwtVc.disclosureObjects.map { it.key }, "birthdate")
assertContains(sdJwtVc.fullPayload.keys, "birthdate")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,9 @@ open class CIProvider : OpenIDCredentialIssuer(
credentialRequest,
CredentialErrorCode.invalid_or_missing_proof, message = "Proof must contain nonce")

val data: IssuanceSessionData = (if (holderDid == null) {
val data: IssuanceSessionData = (if (!tokenCredentialMapping.containsKey(nonce)) {
repeat(10) {
log.debug { "WARNING: RETURNING DEMO/EXAMPLE (= BOGUS) CREDENTIAL: subjectDid or nonce is null (was deferred issuance tried?)" }
log.debug { "WARNING: RETURNING DEMO/EXAMPLE (= BOGUS) CREDENTIAL: nonce is not mapped to issuance request data (was deferred issuance tried?)" }
}
listOf(
IssuanceSessionData(
Expand Down Expand Up @@ -308,7 +308,7 @@ open class CIProvider : OpenIDCredentialIssuer(
issuerKid = issuerDid + "#" + issuerKey.getKeyId()

when (credentialRequest.format) {
CredentialFormat.sd_jwt_vc -> sdJwtVc(holderKey, vc, data, holderDid)
CredentialFormat.sd_jwt_vc -> sdJwtVc(JWKKey.importJWK(holderKey.toString()).getOrNull(), vc, data, holderDid)
else -> nonSdJwtVc(vc, issuerKid, holderDid, holderKey)
}
}.also { log.debug { "Respond VC: $it" } }
Expand Down Expand Up @@ -394,6 +394,7 @@ open class CIProvider : OpenIDCredentialIssuer(
return Pair(subjectDid, nonce)
}

@OptIn(ExperimentalSerializationApi::class)
override fun generateBatchCredentialResponse(
batchCredentialRequest: BatchCredentialRequest,
accessToken: String,
Expand Down Expand Up @@ -443,7 +444,7 @@ open class CIProvider : OpenIDCredentialIssuer(
issuerDid = issuerDid,
subjectDid = subjectDid,
mappings = request.mapping ?: JsonObject(emptyMap()),
additionalJwtHeader = emptyMap(),
additionalJwtHeaders = emptyMap(),
additionalJwtOptions = emptyMap(),
disclosureMap = data.request.selectiveDisclosure
?: SDMap.Companion.generateSDMap(
Expand Down Expand Up @@ -501,31 +502,26 @@ open class CIProvider : OpenIDCredentialIssuer(
}

private suspend fun IssuanceSessionData.sdJwtVc(
holderKey: JsonObject?,
holderKey: JWKKey?,
vc: W3CVC,
data: IssuanceSessionData,
holderDid: String?
) = (holderKey?.let {
SDJwtVC.sign(SDPayload.createSDPayload(vc.toJsonObject(), buildJsonObject {}),
WaltIdJWTCryptoProvider(mapOf(issuerKey.getKeyId() to issuerKey)),
issuerDid = issuerDid.ifEmpty { issuerKey.getKeyId() },
holderKeyJWK = holderKey,
issuerKeyId = issuerKey.getKeyId(),
vct = data.request.credentialConfigurationId,
additionalJwtHeader = data.request.x5Chain?.let {
mapOf("x5c" to JsonArray(it.map { cert -> cert.toJsonElement() }))
} ?: mapOf()
)
} ?: SDJwtVC.sign(
SDPayload.createSDPayload(vc.toJsonObject(), buildJsonObject {}),
WaltIdJWTCryptoProvider(mapOf(issuerKey.getKeyId() to issuerKey)),
issuerDid = issuerDid.ifEmpty { issuerKey.getKeyId() },
holderDid = holderDid!!,
issuerKeyId = issuerKey.getKeyId(),
vct = data.request.credentialConfigurationId,
additionalJwtHeader = data.request.x5Chain?.first()?.let {
mapOf("x5c" to JsonPrimitive(it))
} ?: mapOf())).toString()
) = vc.mergingSdJwtIssue(
issuerKey, issuerDid.ifEmpty { issuerKey.getKeyId() },
holderDid ?: holderKey?.getKeyId() ?: throw IllegalArgumentException("Either holderKey or holderDid must be given"),
request.mapping ?: JsonObject(emptyMap()),
additionalJwtHeaders = request.x5Chain?.let {
mapOf("x5c" to JsonArray(it.map { cert -> cert.toJsonElement() }))
} ?: mapOf(),
additionalJwtOptions = SDJwtVC.defaultPayloadProperties(
issuerDid.ifEmpty { issuerKey.getKeyId() },
JsonObject(holderKey?.let {
buildJsonObject { put("jwk", it.exportJWKObject()) }
} ?: buildJsonObject { put("kid", holderDid) }),
vct = data.request.credentialConfigurationId
),
disclosureMap = request.selectiveDisclosure ?: SDMap(emptyMap())
)

private suspend fun IssuanceSessionData.nonSdJwtVc(
vc: W3CVC,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,11 @@ object IssuanceExamples {
"type": [
"Profile"
],
"id": "did:key:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION FROM CONTEXT (see below)",
"name": "Jobs for the Future (JFF)",
"url": "https://www.jff.org/",
"image": "https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png"
},
"issuanceDate": "2023-07-20T07:05:44Z (THIS WILL BE REPLACED BY DYNAMIC DATA FUNCTION (see below))",
"expirationDate": "WILL BE MAPPED BY DYNAMIC DATA FUNCTION (see below)",
"credentialSubject": {
"id": "did:key:123 (THIS WILL BE REPLACED BY DYNAMIC DATA FUNCTION (see below))",
"type": [
"AchievementSubject"
],
Expand Down Expand Up @@ -118,20 +114,15 @@ object IssuanceExamples {
"givenName":"JOHN",
"id":"identity#bankId"
},
"id":"identity#BankId#3add94f4-28ec-42a1-8704-4e4aa51006b4",
"issued":"2021-08-31T00:00:00Z",
"issuer":{
"id":"did:key:z6MkrHKzgsahxBLyNAbLQyB1pcWNYC9GmywiWPgkrvntAZcj",
"image":{
"id":"https://images.squarespace-cdn.com/content/v1/609c0ddf94bcc0278a7cbdb4/1660296169313-K159K9WX8J8PPJE005HV/Walt+Bot_Logo.png?format=100w",
"type":"Image"
},
"name":"CH Authority",
"type":"Profile",
"url":"https://images.squarespace-cdn.com/content/v1/609c0ddf94bcc0278a7cbdb4/1660296169313-K159K9WX8J8PPJE005HV/Walt+Bot_Logo.png?format=100w"
},
"validFrom":"2021-08-31T00:00:00Z",
"issuanceDate":"2021-08-31T00:00:00Z"
}
}
""".trimIndent()

Expand Down Expand Up @@ -697,9 +688,13 @@ object IssuanceExamples {
{
"fields":
{
"name":
"birthdate":
{
"sd": true
},
"family_name":
{
"sd": false
}
}
}
Expand Down

0 comments on commit 8ba2240

Please sign in to comment.