Skip to content

Commit

Permalink
Support W3C signed jwt credential requests (#193)
Browse files Browse the repository at this point in the history
* Support W3C signed jwt credential requests
* Simplified issuance request DTOs
---------
Co-authored-by: vafeini <[email protected]>
  • Loading branch information
vafeini authored Apr 22, 2024
1 parent e2155e2 commit 3ebbd9c
Show file tree
Hide file tree
Showing 12 changed files with 520 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ internal sealed interface CredentialType {
data class MsoMdocDocType(val doctype: String, val claimSet: MsoMdocClaimSet?) : CredentialType

data class SdJwtVcType(val type: String, val claims: GenericClaimSet?) : CredentialType

data class W3CSignedJwtType(val type: List<String>, val claims: GenericClaimSet?) : CredentialType
}

/**
Expand Down Expand Up @@ -71,15 +73,15 @@ internal sealed interface CredentialIssuanceRequest {

companion object {
internal fun formatBased(
supportedCredential: CredentialConfiguration,
credentialConfiguration: CredentialConfiguration,
claimSet: ClaimSet?,
proof: Proof?,
responseEncryptionSpec: IssuanceResponseEncryptionSpec?,
): FormatBased {
val cd = when (supportedCredential) {
is MsoMdocCredential -> msoMdoc(supportedCredential, claimSet.ensureClaimSet())
is SdJwtVcCredential -> sdJwtVc(supportedCredential, claimSet.ensureClaimSet())
is W3CSignedJwtCredential -> error("Format $FORMAT_W3C_SIGNED_JWT not supported")
val cd = when (credentialConfiguration) {
is MsoMdocCredential -> msoMdoc(credentialConfiguration, claimSet.ensureClaimSet())
is SdJwtVcCredential -> sdJwtVc(credentialConfiguration, claimSet.ensureClaimSet())
is W3CSignedJwtCredential -> w3cSignedJwt(credentialConfiguration, claimSet.ensureClaimSet())
is W3CJsonLdSignedJwtCredential -> error("Format $FORMAT_W3C_JSONLD_SIGNED_JWT not supported")
is W3CJsonLdDataIntegrityCredential -> error("Format $FORMAT_W3C_JSONLD_DATA_INTEGRITY not supported")
}
Expand All @@ -94,13 +96,16 @@ private inline fun <reified C : ClaimSet> ClaimSet?.ensureClaimSet(): C? =
this
} else null

private fun msoMdoc(supportedCredential: MsoMdocCredential, claimSet: MsoMdocClaimSet?): CredentialType.MsoMdocDocType {
private fun msoMdoc(
credentialConfiguration: MsoMdocCredential,
claimSet: MsoMdocClaimSet?,
): CredentialType.MsoMdocDocType {
fun MsoMdocClaimSet.validate() {
if (isNotEmpty()) {
val supportedClaims = supportedCredential.claims
val supportedClaims = credentialConfiguration.claims
ensure(supportedClaims.isNotEmpty()) {
InvalidIssuanceRequest(
"Issuer does not support claims for credential [MsoMdoc-${supportedCredential.docType}]",
"Issuer does not support claims for credential [MsoMdoc-${credentialConfiguration.docType}]",
)
}

Expand All @@ -118,22 +123,22 @@ private fun msoMdoc(supportedCredential: MsoMdocCredential, claimSet: MsoMdocCla

val validClaimSet = claimSet?.apply { validate() }
return CredentialType.MsoMdocDocType(
doctype = supportedCredential.docType,
doctype = credentialConfiguration.docType,
claimSet = validClaimSet,
)
}

private fun sdJwtVc(
supportedCredential: SdJwtVcCredential,
credentialConfiguration: SdJwtVcCredential,
claimSet: GenericClaimSet?,
): CredentialType.SdJwtVcType {
fun GenericClaimSet.validate() {
if (claims.isNotEmpty()) {
val supportedClaims = supportedCredential.claims
val supportedClaims = credentialConfiguration.claims
ensure(!supportedClaims.isNullOrEmpty()) {
InvalidIssuanceRequest(
"Issuer does not support claims for credential " +
"[$FORMAT_SD_JWT_VC-${supportedCredential.type}]",
"[$FORMAT_SD_JWT_VC-${credentialConfiguration.type}]",
)
}
ensure(supportedClaims.keys.containsAll(claims)) {
Expand All @@ -144,7 +149,33 @@ private fun sdJwtVc(

val validClaimSet = claimSet?.apply { validate() }
return CredentialType.SdJwtVcType(
type = supportedCredential.type,
type = credentialConfiguration.type,
claims = validClaimSet,
)
}

private fun w3cSignedJwt(
credentialConfiguration: W3CSignedJwtCredential,
claimSet: GenericClaimSet?,
): CredentialType.W3CSignedJwtType {
fun GenericClaimSet.validate() {
if (claims.isNotEmpty()) {
val supportedClaims = credentialConfiguration.credentialDefinition.credentialSubject
ensure(!supportedClaims.isNullOrEmpty()) {
InvalidIssuanceRequest(
"Issuer does not support claims for credential " +
"[$FORMAT_W3C_SIGNED_JWT-${credentialConfiguration.credentialDefinition.type}]",
)
}
ensure(supportedClaims.keys.containsAll(claims)) {
InvalidIssuanceRequest("Claim names requested are not supported by issuer")
}
}
}

val validClaimSet = claimSet?.apply { validate() }
return CredentialType.W3CSignedJwtType(
type = credentialConfiguration.credentialDefinition.type,
claims = validClaimSet,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,33 @@ package eu.europa.ec.eudi.openid4vci.internal.formats

import eu.europa.ec.eudi.openid4vci.FORMAT_MSO_MDOC
import eu.europa.ec.eudi.openid4vci.FORMAT_SD_JWT_VC
import eu.europa.ec.eudi.openid4vci.FORMAT_W3C_SIGNED_JWT
import eu.europa.ec.eudi.openid4vci.IssuanceResponseEncryptionSpec
import eu.europa.ec.eudi.openid4vci.internal.Proof
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*

internal object IssuanceRequestJsonMapper {
fun asJson(request: CredentialIssuanceRequest): CredentialIssuanceRequestTO = toTransferObject(request)
fun asJson(request: CredentialIssuanceRequest.SingleRequest): SingleCredentialTO = transferObjectOfSingle(request)
fun asJson(request: CredentialIssuanceRequest.BatchRequest): BatchCredentialsTO = toTransferObject(request)
}

private fun toTransferObject(request: CredentialIssuanceRequest): CredentialIssuanceRequestTO = when (request) {
is CredentialIssuanceRequest.BatchRequest ->
request.credentialRequests
.map { transferObjectOfSingle(it) }
.let { CredentialIssuanceRequestTO.BatchCredentialsTO(it) }

is CredentialIssuanceRequest.SingleRequest -> transferObjectOfSingle(request)
}
private fun toTransferObject(request: CredentialIssuanceRequest.BatchRequest): BatchCredentialsTO =
request.credentialRequests
.map { transferObjectOfSingle(it) }
.let { BatchCredentialsTO(it) }

private fun transferObjectOfSingle(
request: CredentialIssuanceRequest.SingleRequest,
): CredentialIssuanceRequestTO.SingleCredentialTO {
): SingleCredentialTO {
val credentialResponseEncryptionSpecTO = request.encryption?.run { transferObject() }

return when (request) {
is CredentialIssuanceRequest.FormatBased ->
when (val credential = request.credential) {
is CredentialType.MsoMdocDocType -> MsoMdocIssuanceRequestTO(
is CredentialType.MsoMdocDocType -> SingleCredentialTO(
format = FORMAT_MSO_MDOC,
proof = request.proof,
credentialResponseEncryptionSpec = credentialResponseEncryptionSpecTO,
docType = credential.doctype,
Expand All @@ -54,7 +52,8 @@ private fun transferObjectOfSingle(
},
)

is CredentialType.SdJwtVcType -> SdJwtVcIssuanceRequestTO(
is CredentialType.SdJwtVcType -> SingleCredentialTO(
format = FORMAT_SD_JWT_VC,
proof = request.proof,
credentialResponseEncryptionSpec = credentialResponseEncryptionSpecTO,
vct = credential.type,
Expand All @@ -66,12 +65,28 @@ private fun transferObjectOfSingle(
}
},
)

is CredentialType.W3CSignedJwtType -> SingleCredentialTO(
format = FORMAT_W3C_SIGNED_JWT,
proof = request.proof,
credentialResponseEncryptionSpec = credentialResponseEncryptionSpecTO,
credentialDefinition = CredentialDefinitionTO(
type = credential.type,
credentialSubject = credential.claims?.let {
buildJsonObject {
it.claims.forEach { claimName ->
put(claimName, JsonObject(emptyMap()))
}
}
},
),
)
}

is CredentialIssuanceRequest.IdentifierBased -> IdentifierBasedIssuanceRequestTO(
is CredentialIssuanceRequest.IdentifierBased -> SingleCredentialTO(
credentialIdentifier = request.credentialId.value,
proof = request.proof,
credentialResponseEncryptionSpec = credentialResponseEncryptionSpecTO,
configurationId = request.credentialId.value,
)
}
}
Expand All @@ -88,22 +103,9 @@ private fun IssuanceResponseEncryptionSpec.transferObject(): CredentialResponseE
}

@Serializable
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator("format")
internal sealed interface CredentialIssuanceRequestTO {

@Serializable
@SerialName("batch-credential-request")
data class BatchCredentialsTO(
@SerialName("credential_requests") val credentialRequests: List<SingleCredentialTO>,
) : CredentialIssuanceRequestTO

@Serializable
sealed interface SingleCredentialTO : CredentialIssuanceRequestTO {
val proof: Proof?
val credentialResponseEncryptionSpec: CredentialResponseEncryptionSpecTO?
}
}
internal data class BatchCredentialsTO(
@SerialName("credential_requests") val credentialRequests: List<SingleCredentialTO>,
)

@Serializable
internal data class CredentialResponseEncryptionSpecTO(
Expand All @@ -113,26 +115,23 @@ internal data class CredentialResponseEncryptionSpecTO(
)

@Serializable
internal data class IdentifierBasedIssuanceRequestTO(
@SerialName("proof") override val proof: Proof? = null,
@SerialName("credential_response_encryption") override val credentialResponseEncryptionSpec: CredentialResponseEncryptionSpecTO? = null,
@SerialName("credential_identifier") val configurationId: String,
) : CredentialIssuanceRequestTO.SingleCredentialTO

@Serializable
@SerialName(FORMAT_MSO_MDOC)
internal data class MsoMdocIssuanceRequestTO(
@SerialName("doctype") val docType: String,
@SerialName("proof") override val proof: Proof? = null,
@SerialName("credential_response_encryption") override val credentialResponseEncryptionSpec: CredentialResponseEncryptionSpecTO? = null,
@SerialName("claims") val claims: JsonObject?,
) : CredentialIssuanceRequestTO.SingleCredentialTO
internal data class CredentialDefinitionTO(
@SerialName("type") val type: List<String>,
@SerialName("credentialSubject") val credentialSubject: JsonObject? = null,
)

@Serializable
@SerialName(FORMAT_SD_JWT_VC)
internal data class SdJwtVcIssuanceRequestTO(
@SerialName("vct") val vct: String,
@SerialName("proof") override val proof: Proof? = null,
@SerialName("credential_response_encryption") override val credentialResponseEncryptionSpec: CredentialResponseEncryptionSpecTO? = null,
internal data class SingleCredentialTO(
@SerialName("credential_identifier") val credentialIdentifier: String? = null,
@SerialName("format") val format: String? = null,
@SerialName("doctype") val docType: String? = null,
@SerialName("vct") val vct: String? = null,
@SerialName("proof") val proof: Proof? = null,
@SerialName("credential_response_encryption") val credentialResponseEncryptionSpec: CredentialResponseEncryptionSpecTO? = null,
@SerialName("claims") val claims: JsonObject? = null,
) : CredentialIssuanceRequestTO.SingleCredentialTO
@SerialName("credential_definition") val credentialDefinition: CredentialDefinitionTO? = null,
) {
init {
require(format != null || credentialIdentifier != null) { "Either format or credentialIdentifier must be set" }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ class IssuanceAuthorizationTest {
val preAuthGrantOffer = """
{
"credential_issuer": "$CREDENTIAL_ISSUER_PUBLIC_URL",
"credential_configuration_ids": ["eu.europa.ec.eudiw.pid_mso_mdoc", "eu.europa.ec.eudiw.pid_vc_sd_jwt"],
"credential_configuration_ids": ["$PID_MsoMdoc", "$PID_SdJwtVC"],
"grants": {
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "eyJhbGciOiJSU0EtFYUaBy",
Expand Down Expand Up @@ -333,7 +333,7 @@ class IssuanceAuthorizationTest {
val preAuthGrantOffer = """
{
"credential_issuer": "$CREDENTIAL_ISSUER_PUBLIC_URL",
"credential_configuration_ids": ["eu.europa.ec.eudiw.pid_mso_mdoc", "eu.europa.ec.eudiw.pid_vc_sd_jwt"],
"credential_configuration_ids": ["$PID_MsoMdoc", "$PID_SdJwtVC"],
"grants": {
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "eyJhbGciOiJSU0EtFYUaBy",
Expand Down Expand Up @@ -498,7 +498,7 @@ class IssuanceAuthorizationTest {
val noProofRequiredOffer = """
{
"credential_issuer": "$CREDENTIAL_ISSUER_PUBLIC_URL",
"credential_configuration_ids": ["MobileDrivingLicense_msoMdoc"],
"credential_configuration_ids": ["$MDL_MsoMdoc"],
"grants": {
"authorization_code": {
"issuer_state": "eyJhbGciOiJSU0EtFYUaBy"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class IssuanceBatchRequestTest {
CertificateIssuanceResponse(
credential = "issued_credential_content_sd_jwt_vc",
),
CertificateIssuanceResponse(
credential = "issued_credential_content_jwt_vc_json",
),
),
cNonce = "wlbQc6pCJp",
cNonceExpiresInSeconds = 86400,
Expand Down Expand Up @@ -98,6 +101,14 @@ class IssuanceBatchRequestTest {
),
)

val claimSet_w3c_signed_jwt = GenericClaimSet(
claims = listOf(
"given_name",
"family_name",
"degree",
),
)

with(issuer) {
when (authorizedRequest) {
is AuthorizedRequest.NoProofRequired -> {
Expand All @@ -110,6 +121,10 @@ class IssuanceBatchRequestTest {
CredentialConfigurationIdentifier(PID_SdJwtVC),
claimSet_sd_jwt_vc,
),
IssuanceRequestPayload.ConfigurationBased(
CredentialConfigurationIdentifier(DEGREE_JwtVcJson),
claimSet_w3c_signed_jwt,
),
)
val submittedRequest = authorizedRequest.requestBatch(batchRequestPayload).getOrThrow()
when (submittedRequest) {
Expand All @@ -132,6 +147,13 @@ class IssuanceBatchRequestTest {
),
proofSigner,
),
Pair(
IssuanceRequestPayload.ConfigurationBased(
CredentialConfigurationIdentifier(DEGREE_JwtVcJson),
claimSet_w3c_signed_jwt,
),
proofSigner,
),
)

val response = proofRequired.requestBatch(credentialMetadataTriples).getOrThrow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
package eu.europa.ec.eudi.openid4vci

import eu.europa.ec.eudi.openid4vci.internal.DeferredIssuanceRequestTO
import eu.europa.ec.eudi.openid4vci.internal.formats.CredentialIssuanceRequestTO
import eu.europa.ec.eudi.openid4vci.internal.formats.SdJwtVcIssuanceRequestTO
import eu.europa.ec.eudi.openid4vci.internal.formats.SingleCredentialTO
import io.ktor.client.engine.mock.*
import io.ktor.client.request.*
import io.ktor.http.*
Expand Down Expand Up @@ -302,7 +301,7 @@ class IssuanceDeferredRequestTest {

private fun respondToCredentialIssuanceRequest(
call: MockRequestHandleScope,
issuanceRequest: SdJwtVcIssuanceRequestTO?,
issuanceRequest: SingleCredentialTO?,
): HttpResponseData =
if (issuanceRequest == null) {
call.respond(
Expand Down Expand Up @@ -354,9 +353,9 @@ class IssuanceDeferredRequestTest {
null
}

private fun asIssuanceRequest(bodyStr: String): SdJwtVcIssuanceRequestTO? =
private fun asIssuanceRequest(bodyStr: String): SingleCredentialTO? =
try {
Json.decodeFromString<CredentialIssuanceRequestTO>(bodyStr) as SdJwtVcIssuanceRequestTO
Json.decodeFromString<SingleCredentialTO>(bodyStr)
} catch (ex: Exception) {
null
}
Expand Down
Loading

0 comments on commit 3ebbd9c

Please sign in to comment.