Skip to content

Commit

Permalink
Wire more bits for draft 14 support.
Browse files Browse the repository at this point in the history
  • Loading branch information
dzarras committed Sep 26, 2024
1 parent 9135873 commit f832695
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class WalletApi(
return when (val response = issueCredential(context, credentialRequest)) {
is IssueCredentialResponse.PlainTO ->
ServerResponse
.status(response.credential?.let { HttpStatus.OK } ?: HttpStatus.ACCEPTED)
.status(response.transactionId?.let { HttpStatus.ACCEPTED } ?: HttpStatus.OK)
.json()
.bodyValueAndAwait(response)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,43 @@ import eu.europa.ec.eudi.pidissuer.port.out.jose.EncryptCredentialResponse
import eu.europa.ec.eudi.pidissuer.port.out.jose.EncryptDeferredResponse
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import java.time.Clock
import java.time.Instant
import java.util.*

/**
* Converts this [JsonElement] to its Nimbus representation.
*/
private fun JsonElement.toNimbus(): Any =
when (this) {
is JsonPrimitive -> content
else -> JSONObjectUtils.parse(Json.encodeToString(this))
}

/**
* Populates either the 'credential' or 'credentials' claim.
*/
private fun JWTClaimsSet.Builder.credentialOrCredentials(
credential: JsonElement? = null,
credentials: JsonArray? = null,
) {
require((credential != null) xor (credentials != null)) {
"exactly one of 'credential' or 'credentials' must be provided"
}

credential?.let {
val value = it.toNimbus()
claim("credential", value)
}
credentials?.let {
val value = it.map { credential -> credential.toNimbus() }
claim("credentials", value)
}
}

/**
* Implementation of [EncryptDeferredResponse] using Nimbus.
*/
Expand All @@ -54,10 +86,7 @@ class EncryptDeferredResponseNimbus(
): Result<DeferredCredentialSuccessResponse.EncryptedJwtIssued> = runCatching {
fun JWTClaimsSet.Builder.toJwtClaims(plain: DeferredCredentialSuccessResponse.PlainTO) {
with(plain) {
val value: Any =
if (credential is JsonPrimitive) credential.content
else JSONObjectUtils.parse(Json.encodeToString(credential))
claim("credential", value)
credentialOrCredentials(plain.credential, plain.credentials)
notificationId?.let { claim("notification_id", it) }
}
}
Expand All @@ -83,12 +112,7 @@ class EncryptCredentialResponseNimbus(
): Result<IssueCredentialResponse.EncryptedJwtIssued> = kotlin.runCatching {
fun JWTClaimsSet.Builder.toJwtClaims(plain: IssueCredentialResponse.PlainTO) {
with(plain) {
this.credential?.let {
val value: Any =
if (it is JsonPrimitive) it.content
else JSONObjectUtils.parse(Json.encodeToString(it))
claim("credential", value)
}
credentialOrCredentials(plain.credential, plain.credentials)
transactionId?.let { claim("transaction_id", it) }
claim("c_nonce", nonce)
claim("c_nonce_expires_in", nonceExpiresIn)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package eu.europa.ec.eudi.pidissuer.adapter.out.mdl
import arrow.core.nonEmptySetOf
import arrow.core.raise.Raise
import arrow.core.raise.ensureNotNull
import arrow.core.toNonEmptyListOrNull
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.ECKey
import com.nimbusds.jose.jwk.JWK
Expand All @@ -29,6 +30,7 @@ import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError.InvalidProof
import eu.europa.ec.eudi.pidissuer.port.out.IssueSpecificCredential
import eu.europa.ec.eudi.pidissuer.port.out.persistence.GenerateNotificationId
import eu.europa.ec.eudi.pidissuer.port.out.persistence.StoreIssuedCredential
import kotlinx.coroutines.*
import kotlinx.serialization.json.JsonPrimitive
import org.slf4j.LoggerFactory
import java.time.Clock
Expand Down Expand Up @@ -316,9 +318,13 @@ class IssueMobileDrivingLicence(
request: CredentialRequest,
credentialIdentifier: CredentialIdentifier?,
expectedCNonce: CNonce,
): CredentialResponse {
): CredentialResponse = coroutineScope {
log.info("Issuing mDL")
val holderKeys = request.unvalidatedProofs.map { holderPubKey(it, expectedCNonce) }
val holderKeys = request.unvalidatedProofs.map {
async(Dispatchers.Default) {
holderPubKey(it, expectedCNonce)
}
}
val licence = ensureNotNull(getMobileDrivingLicenceData(authorizationContext)) {
IssueCredentialError.Unexpected("Unable to fetch mDL data")
}
Expand All @@ -327,7 +333,7 @@ class IssueMobileDrivingLicence(
if (notificationsEnabled) generateNotificationId()
else null

val issuedCredentials = holderKeys.map { holderKey ->
val issuedCredentials = holderKeys.awaitAll().map { holderKey ->
val cbor = encodeMobileDrivingLicenceInCbor(licence, holderKey)
storeIssuedCredential(
IssuedCredential(
Expand All @@ -342,9 +348,12 @@ class IssueMobileDrivingLicence(
),
)
cbor
}.toNonEmptyListOrNull()
ensureNotNull(issuedCredentials) {
IssueCredentialError.Unexpected("Unable to issue mDL")
}

return CredentialResponse.Issued(issuedCredentials.map { JsonPrimitive(it) }, notificationId)
CredentialResponse.Issued(issuedCredentials.map { JsonPrimitive(it) }, notificationId)
.also {
log.info("Successfully issued mDL(s)")
log.debug("Issued mDL(s) data {}", it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package eu.europa.ec.eudi.pidissuer.adapter.out.pid

import arrow.core.nonEmptySetOf
import arrow.core.raise.Raise
import arrow.core.raise.ensureNotNull
import arrow.core.toNonEmptyListOrNull
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.ECKey
import com.nimbusds.jose.jwk.JWK
Expand All @@ -30,6 +32,7 @@ import eu.europa.ec.eudi.pidissuer.port.out.persistence.GenerateNotificationId
import eu.europa.ec.eudi.pidissuer.port.out.persistence.StoreIssuedCredential
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.json.JsonPrimitive
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -279,16 +282,19 @@ class IssueMsoMdocPid(
expectedCNonce: CNonce,
): CredentialResponse = coroutineScope {
log.info("Handling issuance request ...")
val holderPubKey = async(Dispatchers.Default) {
request.unvalidatedProofs.map { holderPubKey(it, expectedCNonce) }
val holderPubKeys = request.unvalidatedProofs.map {
async(Dispatchers.Default) {
holderPubKey(it, expectedCNonce)
}
}

val pidData = async { getPidData(authorizationContext) }
val notificationId =
if (notificationsEnabled) generateNotificationId()
else null

val (pid, pidMetaData) = pidData.await()
val issuedCredentials = holderPubKey.await().map { holderKey ->
val issuedCredentials = holderPubKeys.awaitAll().map { holderKey ->
val cbor = encodePidInCbor(pid, pidMetaData, holderKey).also {
log.info("Issued $it")
}
Expand All @@ -305,6 +311,9 @@ class IssueMsoMdocPid(
),
)
cbor
}.toNonEmptyListOrNull()
ensureNotNull(issuedCredentials) {
IssueCredentialError.Unexpected("Unable to issue PID")
}

CredentialResponse.Issued(issuedCredentials.map { JsonPrimitive(it) }, notificationId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package eu.europa.ec.eudi.pidissuer.adapter.out.pid

import arrow.core.nonEmptySetOf
import arrow.core.raise.Raise
import arrow.core.raise.ensureNotNull
import arrow.core.toNonEmptyListOrNull
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.JWK
import eu.europa.ec.eudi.pidissuer.adapter.out.IssuerSigningKey
Expand All @@ -34,6 +36,7 @@ import eu.europa.ec.eudi.pidissuer.port.out.persistence.StoreIssuedCredential
import eu.europa.ec.eudi.sdjwt.HashAlgorithm
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.json.JsonPrimitive
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -143,15 +146,18 @@ class IssueSdJwtVcPid(
expectedCNonce: CNonce,
): CredentialResponse = coroutineScope {
log.info("Handling issuance request ...")
val holderPubKeys = async(Dispatchers.Default) {
request.unvalidatedProofs.map { holderPubKey(it, expectedCNonce) }
val holderPubKeys = request.unvalidatedProofs.map {
async(Dispatchers.Default) {
holderPubKey(it, expectedCNonce)
}
}

val pidData = async { getPidData(authorizationContext) }
val (pid, pidMetaData) = pidData.await()
val notificationId =
if (notificationsEnabled) generateNotificationId()
else null
val issuedCredentials = holderPubKeys.await().map { holderPubKey ->
val issuedCredentials = holderPubKeys.awaitAll().map { holderPubKey ->
val sdJwt = encodePidInSdJwt.invoke(pid, pidMetaData, holderPubKey)
storeIssuedCredential(
IssuedCredential(
Expand All @@ -166,6 +172,9 @@ class IssueSdJwtVcPid(
),
)
sdJwt
}.toNonEmptyListOrNull()
ensureNotNull(issuedCredentials) {
IssueCredentialError.Unexpected("Unable to issue PID")
}

CredentialResponse.Issued(issuedCredentials.map { JsonPrimitive(it) }, notificationId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import org.slf4j.LoggerFactory

@Serializable
Expand All @@ -46,14 +48,46 @@ sealed interface DeferredCredentialSuccessResponse {
@SerialName("notification_id") val notificationId: String? = null,
) : DeferredCredentialSuccessResponse {
init {
require(credential != null || !credentials.isNullOrEmpty())
if (credential != null) {
require(credentials.isNullOrEmpty())
require((credential != null) xor (credentials != null)) {
"exactly one of 'credential' or 'credentials' must be provided"
}
if (credentials != null) {
require(credential == null)
credential?.also { credential ->
require(credential is JsonObject || (credential is JsonPrimitive && credential.isString)) {
"credential must either be a JsonObject or a string JsonPrimitive"
}
}
credentials?.forEach { credential ->
require(credential is JsonObject || (credential is JsonPrimitive && credential.isString)) {
"credentials must contain either JsonObjects or string JsonPrimitives"
}
}
}

companion object {
/**
* Single credential has been issued.
*/
fun single(
credential: JsonElement,
notificationId: String?,
): PlainTO = PlainTO(
credential = credential,
credentials = null,
notificationId = notificationId,
)

/**
* Multiple credentials have been issued.
*/
fun multiple(
credentials: JsonArray,
notificationId: String?,
): PlainTO = PlainTO(
credential = null,
credentials = credentials,
notificationId = notificationId,
)
}
}

/**
Expand Down Expand Up @@ -94,17 +128,20 @@ class GetDeferredCredential(
is LoadDeferredCredentialResult.IssuancePending -> raise(GetDeferredCredentialErrorTO.IssuancePending)
is LoadDeferredCredentialResult.InvalidTransactionId -> raise(GetDeferredCredentialErrorTO.InvalidTransactionId)
is LoadDeferredCredentialResult.Found -> {
val (c, cs) = when (credential.credentials.size) {
1 -> credential.credentials.head to null
else -> null to JsonArray(credential.credentials)
val plain = when (credential.credentials.size) {
1 -> DeferredCredentialSuccessResponse.PlainTO.single(
credential.credentials.head,
credential.notificationId?.value,
)
else -> DeferredCredentialSuccessResponse.PlainTO.multiple(
JsonArray(credential.credentials),
credential.notificationId?.value,
)
}

when (responseEncryption) {
RequestedResponseEncryption.NotRequired ->
DeferredCredentialSuccessResponse.PlainTO(c, cs, credential.notificationId?.value)
is RequestedResponseEncryption.Required -> {
val plain = DeferredCredentialSuccessResponse.PlainTO(c, cs, credential.notificationId?.value)
encryptCredentialResponse(plain, responseEncryption).getOrThrow()
}
RequestedResponseEncryption.NotRequired -> plain
is RequestedResponseEncryption.Required -> encryptCredentialResponse(plain, responseEncryption).getOrThrow()
}
}
}
Expand Down
Loading

0 comments on commit f832695

Please sign in to comment.