Skip to content

Commit

Permalink
Support SD-JWT VC and MSO_MDOC formats within a single OpenID4VP resp…
Browse files Browse the repository at this point in the history
…onse.
  • Loading branch information
marinaioannou committed Jan 14, 2025
1 parent 5f30772 commit 3e7139c
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,54 +56,72 @@ class OpenId4VpRequestProcessor(
.flatMap { it.format?.jsonObject()?.keys ?: emptySet() }
.toSet()
require(requestedFormats.isNotEmpty()) { "No format is requested" }
require(requestedFormats.size == 1) { "Currently, only one format is supported at a time" }
requestedFormats.forEach {
require(it in listOf("mso_mdoc", "vc+sd-jwt")) { "Unsupported format: $it" }
}

return when (val requestedFormat = requestedFormats.first()) {
"mso_mdoc" -> {
val requestedDocuments: RequestedDocuments =
request.resolvedRequestObject.presentationDefinition.inputDescriptors
.map { descriptor -> descriptor.toParsedRequestedMsoMdocDocument(request) }
.let { helper.getRequestedDocuments(it) }

val msoMdocNonce = generateMdocGeneratedNonce()
val sessionTranscriptBytes =
request.resolvedRequestObject.getSessionTranscriptBytes(msoMdocNonce)

ProcessedMsoMdocOpenId4VpRequest(
resolvedRequestObject = request.resolvedRequestObject,
processedDeviceRequest = ProcessedDeviceRequest(
documentManager = documentManager,
requestedDocuments = requestedDocuments,
sessionTranscript = sessionTranscriptBytes
),
msoMdocNonce = msoMdocNonce
)
}
return if (requestedFormats.size == 1 && requestedFormats.first() == "mso_mdoc") {
val requestedDocuments: RequestedDocuments =
request.resolvedRequestObject.presentationDefinition.inputDescriptors
.map { descriptor -> descriptor.toParsedRequestedMsoMdocDocument(request) }
.let { helper.getRequestedDocuments(it) }

val msoMdocNonce = generateMdocGeneratedNonce()
val sessionTranscriptBytes =
request.resolvedRequestObject.getSessionTranscriptBytes(msoMdocNonce)

ProcessedMsoMdocOpenId4VpRequest(
resolvedRequestObject = request.resolvedRequestObject,
processedDeviceRequest = ProcessedDeviceRequest(
documentManager = documentManager,
requestedDocuments = requestedDocuments,
sessionTranscript = sessionTranscriptBytes
),
msoMdocNonce = msoMdocNonce
)
} else {
val inputDescriptorMap: MutableMap<InputDescriptorId, List<DocumentId>> = mutableMapOf()
var mdocGeneratedNonce: String? = null
val requestedDocuments = RequestedDocuments(
request.resolvedRequestObject.presentationDefinition.inputDescriptors
// NOTE: mso_mdoc and vc+sd-jwt are supported, other formats are ignored
.filter {
it.format?.jsonObject()?.keys?.first() in listOf(
"mso_mdoc",
"vc+sd-jwt"
)
}
.flatMap { inputDescriptor ->
// NOTE: one format is supported per input descriptor
when (inputDescriptor.format?.jsonObject()?.keys?.first()) {
"mso_mdoc" -> {
inputDescriptor.toParsedRequestedMsoMdocDocument(request)
.let { helper.getRequestedDocuments(listOf(it)) }
.also {
mdocGeneratedNonce =
mdocGeneratedNonce ?: generateMdocGeneratedNonce()
}.also { requestedDoc ->
inputDescriptorMap[inputDescriptor.id] =
requestedDoc.map { it.documentId }.toList()
}
}

"vc+sd-jwt" -> {
val inputDescriptorMap: MutableMap<InputDescriptorId, List<DocumentId>> = mutableMapOf()
val requestedDocuments =
RequestedDocuments(request.resolvedRequestObject.presentationDefinition.inputDescriptors
.flatMap { descriptor ->
descriptor.toParsedRequestedSdJwtVcDocument(
request
).also { requestedDoc ->
inputDescriptorMap[descriptor.id] = requestedDoc.map { it.documentId }.toList()
"vc+sd-jwt" -> {
inputDescriptor.toParsedRequestedSdJwtVcDocument(request)
.also { requestedDoc ->
inputDescriptorMap[inputDescriptor.id] =
requestedDoc.map { it.documentId }.toList()
}
}
}
.toList())
ProcessedGenericOpenId4VpRequest(
documentManager,
request.resolvedRequestObject,
inputDescriptorMap,
requestedDocuments
)
}

else -> throw IllegalArgumentException("Unsupported format: $requestedFormat")
else -> throw IllegalArgumentException("Unsupported format ${inputDescriptor.format}")
}
}.toList()
)
ProcessedGenericOpenId4VpRequest(
documentManager,
request.resolvedRequestObject,
inputDescriptorMap,
requestedDocuments,
mdocGeneratedNonce
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import eu.europa.ec.eudi.iso18013.transfer.response.device.DeviceResponse
import eu.europa.ec.eudi.iso18013.transfer.response.device.ProcessedDeviceRequest
import eu.europa.ec.eudi.openid4vp.Consensus
import eu.europa.ec.eudi.openid4vp.ResolvedRequestObject
import eu.europa.ec.eudi.openid4vp.VerifiablePresentation
import eu.europa.ec.eudi.openid4vp.VpToken
import eu.europa.ec.eudi.prex.DescriptorMap
import eu.europa.ec.eudi.prex.Id
Expand All @@ -49,9 +50,11 @@ import eu.europa.ec.eudi.sdjwt.vc.ClaimPath
import eu.europa.ec.eudi.wallet.document.DocumentId
import eu.europa.ec.eudi.wallet.document.DocumentManager
import eu.europa.ec.eudi.wallet.document.IssuedDocument
import eu.europa.ec.eudi.wallet.document.format.MsoMdocFormat
import eu.europa.ec.eudi.wallet.document.format.SdJwtVcFormat
import eu.europa.ec.eudi.wallet.issue.openid4vci.toJoseEncoded
import kotlinx.coroutines.runBlocking
import eu.europa.ec.eudi.wallet.internal.OpenId4VpUtils.getSessionTranscriptBytes
import java.util.Base64
import java.util.Date
import java.util.UUID
Expand Down Expand Up @@ -112,59 +115,89 @@ class ProcessedGenericOpenId4VpRequest(
private val resolvedRequestObject: ResolvedRequestObject,
private val inputDescriptorMap : Map<InputDescriptorId, List<DocumentId>>,
requestedDocuments: RequestedDocuments,
val msoMdocNonce: String?
) : RequestProcessor.ProcessedRequest.Success(requestedDocuments) {
override fun generateResponse(
disclosedDocuments: DisclosedDocuments,
signatureAlgorithm: Algorithm?
): ResponseResult {
return try {
require(resolvedRequestObject is ResolvedRequestObject.OpenId4VPAuthorization)
val sdJwtVCsForPresentation = disclosedDocuments.map {
val document = documentManager.getValidIssuedSdJwtVcDocumentById(it.documentId)
require(document.format is SdJwtVcFormat)
// TODO: support SD JWT Vc && mDoc format in one response
val issuedSdJwt = SdJwt.unverifiedIssuanceFrom(String(document.issuerProvidedData)).getOrThrow()
document.id to (issuedSdJwt.present(it.disclosedItems.map { disclosedItem ->
ClaimPath.claim(disclosedItem.elementIdentifier)
}.toSet())?.run {
// check if cnf is present and present with key binding
issuedSdJwt.jwt.second["cnf"]?.run {
presentWithKeyBinding(
signatureAlgorithm ?: Algorithm.ES256,
document,
it.keyUnlockData,
resolvedRequestObject.client.id,
resolvedRequestObject.nonce,
Date()
val verifiablePresentations = disclosedDocuments.map { disclosedDocument ->
val document =
documentManager.getValidIssuedDocumentById(disclosedDocument.documentId)
when (document.format) {
is SdJwtVcFormat -> {
val issuedSdJwt =
SdJwt.unverifiedIssuanceFrom(String(document.issuerProvidedData))
.getOrThrow()
document.id to VerifiablePresentation.Generic(
issuedSdJwt.present(disclosedDocument.disclosedItems.map { disclosedItem ->
ClaimPath.claim(disclosedItem.elementIdentifier)
}.toSet())?.run {
// check if cnf is present and present with key binding
issuedSdJwt.jwt.second["cnf"]?.run {
presentWithKeyBinding(
signatureAlgorithm ?: Algorithm.ES256,
document,
disclosedDocument.keyUnlockData,
resolvedRequestObject.client.id,
resolvedRequestObject.nonce,
Date()
)
} ?: serialize()
}
?: return ResponseResult.Failure(IllegalArgumentException("Failed to create SD JWT VC presentation")))
}

is MsoMdocFormat -> {
val deviceResponse = ProcessedDeviceRequest(
documentManager = documentManager,
sessionTranscript = resolvedRequestObject.getSessionTranscriptBytes(
msoMdocNonce!!
),
requestedDocuments = RequestedDocuments(requestedDocuments.filter { it.documentId == disclosedDocument.documentId })
).generateResponse(
DisclosedDocuments(disclosedDocument),
signatureAlgorithm
).getOrThrow() as DeviceResponse
document.id to VerifiablePresentation.MsoMdoc(
Base64.getUrlEncoder().withoutPadding()
.encodeToString(deviceResponse.deviceResponseBytes)
)
} ?: serialize()
} ?: return ResponseResult.Failure(IllegalArgumentException("Failed to create SD JWT VC presentation")))
}
}
}.toList()
val vpToken = VpToken.Generic(*(sdJwtVCsForPresentation.map { it.second }).toTypedArray())
val vpToken = VpToken((verifiablePresentations.map { it.second }).toList(),
msoMdocNonce?.let { Base64URL.encode(it) })
val presentationDefinition = resolvedRequestObject.presentationDefinition
val consensus = Consensus.PositiveConsensus.VPTokenConsensus(
vpToken = vpToken,
presentationSubmission = PresentationSubmission(
id = Id(UUID.randomUUID().toString()),
definitionId = presentationDefinition.id,
descriptorMaps = inputDescriptorMap.entries.flatMap { inputDescriptor ->
sdJwtVCsForPresentation.mapIndexed { index, sdjwt ->
inputDescriptor.value.takeIf { it.contains(sdjwt.first) }?.let {
verifiablePresentations.mapIndexed { index, vp ->
inputDescriptor.value.takeIf { it.contains(vp.first) }?.let {
DescriptorMap(
id = inputDescriptor.key,
format = "vc+sd-jwt",
path = JsonPath.jsonPath(if (sdJwtVCsForPresentation.size > 1) "$[$index]" else "$")!!
format = when (documentManager.getValidIssuedDocumentById(vp.first).format) {
is MsoMdocFormat -> "mso_mdoc"
is SdJwtVcFormat -> "vc+sd-jwt"
},
path = JsonPath.jsonPath(if (verifiablePresentations.size > 1) "$[$index]" else "$")!!
)
}
}
}.filterNotNull()
))
)
)
ResponseResult.Success(
OpenId4VpResponse.GenericResponse(
resolvedRequestObject = resolvedRequestObject,
consensus = consensus,
vpToken = vpToken,
response = sdJwtVCsForPresentation.map { it.second }.toList()
response = verifiablePresentations.map { it.second.toString() }.toList()
)
)
} catch (e: Throwable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ import kotlinx.datetime.Clock
import kotlinx.datetime.toJavaInstant

@JvmSynthetic
internal fun DocumentManager.getValidIssuedSdJwtVcDocumentById(documentId: DocumentId): IssuedDocument {
internal fun DocumentManager.getValidIssuedDocumentById(documentId: DocumentId): IssuedDocument {
return (getDocumentById(documentId)
?.takeIf { it is IssuedDocument }
?.takeIf { it.format is SdJwtVcFormat }
?.takeIf { !it.isKeyInvalidated } as? IssuedDocument)
?.takeIf { it.isValidAt(Clock.System.now().toJavaInstant()) }
?: throw IllegalArgumentException("Invalid document")
Expand Down

0 comments on commit 3e7139c

Please sign in to comment.