From 9ad2ffb5b93ac5ceb497aae96be8a35d3e0506df Mon Sep 17 00:00:00 2001 From: Severin Stampler Date: Tue, 12 Nov 2024 15:22:25 +0100 Subject: [PATCH 1/4] let OpenID4VCI lib user define which value should go into the "iss" property of the sd-jwt-vc --- .../src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt | 5 ++--- .../src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt index 181c9a1bd..eeb059908 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt +++ b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt @@ -385,7 +385,7 @@ object OpenID4VCI { suspend fun generateSdJwtVC(credentialRequest: CredentialRequest, credentialData: JsonObject, dataMapping: JsonObject?, - selectiveDisclosure: SDMap?, vct: String, + selectiveDisclosure: SDMap?, vct: String, issuerId: String, issuerDid: String?, issuerKid: String?, x5Chain: List?, issuerKey: Key): String { val proofHeader = credentialRequest.proof?.jwt?.let { JwtUtils.parseJWTHeader(it) } ?: throw CredentialError( @@ -417,8 +417,7 @@ object OpenID4VCI { ?: throw IllegalArgumentException("Either holderKey or holderDid must be given") val defaultPayloadProperties = defaultPayloadProperties( - issuerDid ?: issuerKid ?: issuerKey.getKeyId(), - cnf, vct, null, null, null, null) + issuerId, cnf, vct, null, null, null, null) val undisclosedPayload = sdPayload.undisclosedPayload.plus(defaultPayloadProperties).let { JsonObject(it) } val fullPayload = sdPayload.fullPayload.plus(defaultPayloadProperties).let { JsonObject(it) } diff --git a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt index 05bfb08aa..176cfd861 100644 --- a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt +++ b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt @@ -238,7 +238,7 @@ open class CIProvider( CredentialFormat.sd_jwt_vc -> OpenID4VCI.generateSdJwtVC(credentialRequest, vc, request.mapping, request.selectiveDisclosure, vct = metadata.credentialConfigurationsSupported?.get(request.credentialConfigurationId)?.vct ?: throw ConfigurationException( ConfigException("No vct configured for given credential configuration id: ${request.credentialConfigurationId}") - ), issuerDid, issuerKid, request.x5Chain, resolvedIssuerKey).toString() + ), issuerDid ?: issuerKid, issuerDid, issuerKid, request.x5Chain, resolvedIssuerKey).toString() else -> OpenID4VCI.generateW3CJwtVC(credentialRequest, vc, request.mapping, request.selectiveDisclosure, issuerDid, issuerKid, request.x5Chain, resolvedIssuerKey) } From 8bd93ba9331264bc445ea02b9230ff126b58192c Mon Sep 17 00:00:00 2001 From: Severin Stampler Date: Tue, 12 Nov 2024 16:44:03 +0100 Subject: [PATCH 2/4] fix jwt-vc issuer metadata url generation --- .../src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt | 6 ++++-- .../src/test/kotlin/LspPotentialIssuance.kt | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt index eeb059908..05c1da766 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt +++ b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt @@ -124,9 +124,11 @@ object OpenID4VCI { appendPathSegments(".well-known", "oauth-authorization-server") }.buildString() - fun getJWTIssuerProviderMetadataUrl(baseUrl: String) = URLBuilder(baseUrl).apply { + fun getJWTVCIssuerProviderMetadataUrl(issUrl: String) = Url(issUrl).let { URLBuilder(it.protocolWithAuthority).apply { appendPathSegments(".well-known", "jwt-vc-issuer") - }.buildString() + if(it.fullPath.isNotEmpty()) + appendPathSegments(it.fullPath.substringAfter("/")) + }.buildString() } suspend fun resolveCIProviderMetadata(credOffer: CredentialOffer) = http.get(getCIProviderMetadataUrl(credOffer)).bodyAsText().let { OpenIDProviderMetadata.fromJSONString(it) diff --git a/waltid-services/waltid-e2e-tests/src/test/kotlin/LspPotentialIssuance.kt b/waltid-services/waltid-e2e-tests/src/test/kotlin/LspPotentialIssuance.kt index 56eb7929a..658d2a683 100644 --- a/waltid-services/waltid-e2e-tests/src/test/kotlin/LspPotentialIssuance.kt +++ b/waltid-services/waltid-e2e-tests/src/test/kotlin/LspPotentialIssuance.kt @@ -248,7 +248,7 @@ class LspPotentialIssuance(val client: HttpClient) { // ### get issuer metadata, steps 7-10 val providerMetadataUri = OpenID4VCI.getCIProviderMetadataUrl(parsedOffer.credentialIssuer) - val jwtIssuerMetadataUri = OpenID4VCI.getJWTIssuerProviderMetadataUrl(parsedOffer.credentialIssuer) + val jwtIssuerMetadataUri = OpenID4VCI.getJWTVCIssuerProviderMetadataUrl(parsedOffer.credentialIssuer) val oAuthMetadataUri = OpenID4VCI.getOAuthProviderMetadataUrl(parsedOffer.credentialIssuer) val providerMetadata = client.get(providerMetadataUri).bodyAsText().let { OpenIDProviderMetadata.fromJSONString(it) } val oauthMetadata = client.get(oAuthMetadataUri).body() From fe2d49983e0e37f4cf37382f219aee6e40febb29 Mon Sep 17 00:00:00 2001 From: Severin Stampler Date: Tue, 12 Nov 2024 16:54:34 +0100 Subject: [PATCH 3/4] fix jwt-vc issuer metadata url generation --- .../waltid-e2e-tests/src/test/kotlin/LspPotentialIssuance.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/waltid-services/waltid-e2e-tests/src/test/kotlin/LspPotentialIssuance.kt b/waltid-services/waltid-e2e-tests/src/test/kotlin/LspPotentialIssuance.kt index 658d2a683..687127435 100644 --- a/waltid-services/waltid-e2e-tests/src/test/kotlin/LspPotentialIssuance.kt +++ b/waltid-services/waltid-e2e-tests/src/test/kotlin/LspPotentialIssuance.kt @@ -30,6 +30,7 @@ import id.walt.oid4vc.requests.TokenRequest import id.walt.oid4vc.responses.CredentialResponse import id.walt.oid4vc.responses.TokenResponse import id.walt.oid4vc.util.randomUUID +import id.walt.sdjwt.JWTVCIssuerMetadata import id.walt.sdjwt.SDJwtVC import id.walt.verifier.lspPotential.LspPotentialVerificationInterop import io.ktor.client.* @@ -252,7 +253,7 @@ class LspPotentialIssuance(val client: HttpClient) { val oAuthMetadataUri = OpenID4VCI.getOAuthProviderMetadataUrl(parsedOffer.credentialIssuer) val providerMetadata = client.get(providerMetadataUri).bodyAsText().let { OpenIDProviderMetadata.fromJSONString(it) } val oauthMetadata = client.get(oAuthMetadataUri).body() - val jwtIssuerMetadata = client.get(jwtIssuerMetadataUri).body() + val jwtIssuerMetadata = client.get(jwtIssuerMetadataUri).body() assertNotNull(providerMetadata.credentialConfigurationsSupported) assertNotNull(providerMetadata.credentialEndpoint) assertNotNull(jwtIssuerMetadata.issuer) From 162aacbff127c2077a3de80073d87802c27a959e Mon Sep 17 00:00:00 2001 From: Severin Stampler Date: Thu, 14 Nov 2024 13:35:36 +0100 Subject: [PATCH 4/4] remove code duplication to find kid header, in enterprise and lib --- .../id/walt/credentials/issuance/Issuer.kt | 18 +++++++++++++++--- .../kotlin/id/walt/oid4vc/OpenID4VCI.kt | 8 ++++---- .../id/walt/issuer/issuance/CIProvider.kt | 12 ++---------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/waltid-libraries/credentials/waltid-verifiable-credentials/src/commonMain/kotlin/id/walt/credentials/issuance/Issuer.kt b/waltid-libraries/credentials/waltid-verifiable-credentials/src/commonMain/kotlin/id/walt/credentials/issuance/Issuer.kt index 1e26bdf55..f391204d0 100644 --- a/waltid-libraries/credentials/waltid-verifiable-credentials/src/commonMain/kotlin/id/walt/credentials/issuance/Issuer.kt +++ b/waltid-libraries/credentials/waltid-verifiable-credentials/src/commonMain/kotlin/id/walt/credentials/issuance/Issuer.kt @@ -61,7 +61,6 @@ object Issuer { suspend fun W3CVC.mergingJwtIssue( issuerKey: Key, issuerDid: String?, - issuerKid: String? = null, subjectDid: String, mappings: JsonObject, @@ -79,7 +78,7 @@ object Issuer { w3cVc.signJws( issuerKey = issuerKey, issuerDid = issuerDid, - issuerKid = issuerKid, + issuerKid = getKidHeader(issuerKey, issuerDid), subjectDid = subjectDid, additionalJwtHeader = additionalJwtHeader.toMutableMap().apply { put("typ", "JWT".toJsonElement()) @@ -114,7 +113,7 @@ object Issuer { ).run { w3cVc.signSdJwt( issuerKey = issuerKey, - issuerKeyId = issuerDid ?: issuerKey.getKeyId(), + issuerKeyId = getKidHeader(issuerKey, issuerDid), subjectDid = subjectDid, disclosureMap = disclosureMap, additionalJwtHeaders = additionalJwtHeaders.toMutableMap().apply { @@ -186,4 +185,17 @@ object Issuer { return IssuanceInformation(vc, jwtRes) } + + @JvmBlocking + @JvmAsync + @JsPromise + @JsExport.Ignore + suspend fun getKidHeader(issuerKey: Key, issuerDid: String? = null): String { + return if(!issuerDid.isNullOrEmpty()) { + if (issuerDid.startsWith("did:key")) + issuerDid + "#" + issuerDid.removePrefix("did:key:") + else + issuerDid + "#" + issuerKey.getKeyId() + } else issuerKey.getKeyId() + } } diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt index 05c1da766..9ca7941b5 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt +++ b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt @@ -1,6 +1,7 @@ package id.walt.oid4vc import cbor.Cbor +import id.walt.credentials.issuance.Issuer.getKidHeader import id.walt.credentials.issuance.Issuer.mergingJwtIssue import id.walt.credentials.issuance.Issuer.mergingSdJwtIssue import id.walt.credentials.issuance.dataFunctions @@ -388,7 +389,7 @@ object OpenID4VCI { suspend fun generateSdJwtVC(credentialRequest: CredentialRequest, credentialData: JsonObject, dataMapping: JsonObject?, selectiveDisclosure: SDMap?, vct: String, issuerId: String, - issuerDid: String?, issuerKid: String?, x5Chain: List?, + issuerDid: String?, x5Chain: List?, issuerKey: Key): String { val proofHeader = credentialRequest.proof?.jwt?.let { JwtUtils.parseJWTHeader(it) } ?: throw CredentialError( credentialRequest, CredentialErrorCode.invalid_or_missing_proof, message = "Proof must be JWT proof" @@ -424,7 +425,7 @@ object OpenID4VCI { val fullPayload = sdPayload.fullPayload.plus(defaultPayloadProperties).let { JsonObject(it) } val headers = mapOf( - "kid" to issuerKid, + "kid" to getKidHeader(issuerKey, issuerDid), "typ" to SD_JWT_VC_TYPE_HEADER ).plus(x5Chain?.let { mapOf("x5c" to JsonArray(it.map { cert -> cert.toJsonElement() })) @@ -439,7 +440,7 @@ object OpenID4VCI { suspend fun generateW3CJwtVC(credentialRequest: CredentialRequest, credentialData: JsonObject, dataMapping: JsonObject?, - selectiveDisclosure: SDMap?, issuerDid: String?, issuerKid: String?, + selectiveDisclosure: SDMap?, issuerDid: String?, x5Chain: List?, issuerKey: Key): String { val proofHeader = credentialRequest.proof?.jwt?.let { JwtUtils.parseJWTHeader(it) } ?: throw CredentialError( credentialRequest, CredentialErrorCode.invalid_or_missing_proof, message = "Proof must be JWT proof" @@ -453,7 +454,6 @@ object OpenID4VCI { true -> vc.mergingJwtIssue( issuerKey = issuerKey, issuerDid = issuerDid, - issuerKid = issuerKid, subjectDid = holderDid ?: "", mappings = dataMapping ?: JsonObject(emptyMap()), additionalJwtHeader = additionalJwtHeaders, diff --git a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt index 176cfd861..bb9ae9651 100644 --- a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt +++ b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt @@ -224,23 +224,15 @@ open class CIProvider( val resolvedIssuerKey = KeyManager.resolveSerializedKey(request.issuerKey) request.run { - var issuerKid = issuerDid ?: resolvedIssuerKey.getKeyId() - if(!issuerDid.isNullOrEmpty()) { - if (issuerDid.startsWith("did:key") && issuerDid.length == 186) // EBSI conformance corner case when issuer uses did:key instead of did:ebsi and no trust framework is defined - issuerKid = issuerDid + "#" + issuerDid.removePrefix("did:key:") - else if (issuerDid.startsWith("did:ebsi")) - issuerKid = issuerDid + "#" + resolvedIssuerKey.getKeyId() - } - val holderKeyJWK = JWKKey.importJWK(holderKey.toString()).getOrNull()?.exportJWKObject()?.plus("kid" to JWKKey.importJWK(holderKey.toString()).getOrThrow().getKeyId())?.toJsonObject() when (credentialFormat) { CredentialFormat.sd_jwt_vc -> OpenID4VCI.generateSdJwtVC(credentialRequest, vc, request.mapping, request.selectiveDisclosure, vct = metadata.credentialConfigurationsSupported?.get(request.credentialConfigurationId)?.vct ?: throw ConfigurationException( ConfigException("No vct configured for given credential configuration id: ${request.credentialConfigurationId}") - ), issuerDid ?: issuerKid, issuerDid, issuerKid, request.x5Chain, resolvedIssuerKey).toString() + ), issuerDid ?: baseUrl, issuerDid, request.x5Chain, resolvedIssuerKey).toString() else -> OpenID4VCI.generateW3CJwtVC(credentialRequest, vc, request.mapping, request.selectiveDisclosure, - issuerDid, issuerKid, request.x5Chain, resolvedIssuerKey) + issuerDid, request.x5Chain, resolvedIssuerKey) } }.also { log.debug { "Respond VC: $it" } } }))