Skip to content

Commit

Permalink
Response encryption for deferred credential issuance.
Browse files Browse the repository at this point in the history
  • Loading branch information
vafeini committed May 29, 2024
1 parent 12034ee commit a82c3c3
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,9 @@ fun beans(clock: Clock) = beans {
bean {
IssueCredential(clock, ref(), ref(), ref(), ref(), ref(), ref())
}
bean(::GetDeferredCredential)
bean {
GetDeferredCredential(ref(), ref())
}
bean {
CreateCredentialsOffer(ref(), credentialsOfferUri)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package eu.europa.ec.eudi.pidissuer.adapter.input.web

import arrow.core.NonEmptySet
import arrow.core.getOrElse
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
Expand Down Expand Up @@ -95,10 +96,23 @@ class WalletApi(

private suspend fun handleGetDeferredCredential(req: ServerRequest): ServerResponse = coroutineScope {
val requestTO = req.awaitBody<DeferredCredentialRequestTO>()
either { getDeferredCredential(requestTO) }.fold(
ifLeft = { error -> ServerResponse.badRequest().json().bodyValueAndAwait(error) },
ifRight = { credential -> ServerResponse.ok().json().bodyValueAndAwait(credential) },
)
either {
when (val deferredResponse = getDeferredCredential(requestTO)) {
is DeferredCredentialSuccessResponse.EncryptedJwtIssued ->
ServerResponse
.ok()
.contentType(APPLICATION_JWT)
.bodyValueAndAwait(deferredResponse.jwt)

is DeferredCredentialSuccessResponse.PlainTO ->
ServerResponse
.ok()
.json()
.bodyValueAndAwait(deferredResponse)
}
}.getOrElse { error ->
ServerResponse.badRequest().json().bodyValueAndAwait(error)
}
}

private suspend fun handleHelloHolder(req: ServerRequest): ServerResponse = coroutineScope {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,11 @@ import com.nimbusds.jose.crypto.RSAEncrypter
import com.nimbusds.jose.jwk.ECKey
import com.nimbusds.jose.jwk.JWK
import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.util.JSONObjectUtils
import com.nimbusds.jwt.EncryptedJWT
import com.nimbusds.jwt.JWTClaimsSet
import eu.europa.ec.eudi.pidissuer.domain.CredentialIssuerId
import eu.europa.ec.eudi.pidissuer.domain.RequestedResponseEncryption
import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialResponse
import eu.europa.ec.eudi.pidissuer.port.out.jose.EncryptCredentialResponse
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonPrimitive
import java.time.Clock
import java.time.Instant
import java.util.*
Expand All @@ -45,16 +40,15 @@ class EncryptCredentialResponseWithNimbus(
) : EncryptCredentialResponse {

override fun invoke(
response: IssueCredentialResponse.PlainTO,
parameters: RequestedResponseEncryption.Required,
): Result<IssueCredentialResponse.EncryptedJwtIssued> = runCatching {
responseAsJwtClaims: JWTClaimsSet.Builder.() -> Unit,
): Result<String> = runCatching {
val jweHeader = parameters.asHeader()
val jwtClaimSet = response.asJwtClaimSet(clock.instant())
val jwtClaimSet = asJwtClaimSet(clock.instant(), responseAsJwtClaims)

val jwt = EncryptedJWT(jweHeader, jwtClaimSet)
EncryptedJWT(jweHeader, jwtClaimSet)
.apply { encrypt(parameters.encryptionJwk) }
.serialize()
IssueCredentialResponse.EncryptedJwtIssued(jwt)
}

private fun RequestedResponseEncryption.Required.asHeader() =
Expand All @@ -64,20 +58,11 @@ class EncryptCredentialResponseWithNimbus(
type(JOSEObjectType.JWT)
}.build()

private fun IssueCredentialResponse.PlainTO.asJwtClaimSet(iat: Instant) =
private fun asJwtClaimSet(iat: Instant, responseAsJwtClaims: JWTClaimsSet.Builder.() -> Unit) =
JWTClaimsSet.Builder().apply {
issuer(issuer.externalForm)
issueTime(Date.from(iat))
credential?.let {
val value: Any =
if (it is JsonPrimitive) it.content
else JSONObjectUtils.parse(Json.encodeToString(it))
claim("credential", value)
}
transactionId?.let { claim("transaction_id", it) }
claim("c_nonce", nonce)
claim("c_nonce_expires_in", nonceExpiresIn)
notificationId?.let { claim("notification_id", it) }
this.responseAsJwtClaims()
}.build()

private fun EncryptedJWT.encrypt(jwk: JWK) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package eu.europa.ec.eudi.pidissuer.adapter.out.persistence

import eu.europa.ec.eudi.pidissuer.domain.CredentialResponse
import eu.europa.ec.eudi.pidissuer.domain.RequestedResponseEncryption
import eu.europa.ec.eudi.pidissuer.domain.TransactionId
import eu.europa.ec.eudi.pidissuer.port.out.persistence.LoadDeferredCredentialByTransactionId
import eu.europa.ec.eudi.pidissuer.port.out.persistence.LoadDeferredCredentialResult
Expand All @@ -27,7 +28,8 @@ import org.slf4j.LoggerFactory

private val log = LoggerFactory.getLogger(InMemoryDeferredCredentialRepository::class.java)
class InMemoryDeferredCredentialRepository(
private val data: MutableMap<TransactionId, CredentialResponse.Issued<JsonElement>?> = mutableMapOf(),
private val data: MutableMap<TransactionId, Pair<CredentialResponse.Issued<JsonElement>, RequestedResponseEncryption>?> =
mutableMapOf(),
) {

private val mutex = Mutex()
Expand All @@ -37,20 +39,20 @@ class InMemoryDeferredCredentialRepository(
mutex.withLock(this) {
if (data.containsKey(transactionId)) {
data[transactionId]
?.let { LoadDeferredCredentialResult.Found(it) }
?.let { (credential, encryption) -> LoadDeferredCredentialResult.Found(credential, encryption) }
?: LoadDeferredCredentialResult.IssuancePending
} else LoadDeferredCredentialResult.InvalidTransactionId
}
}

val storeDeferredCredential: StoreDeferredCredential =
StoreDeferredCredential { transactionId, credential ->
StoreDeferredCredential { transactionId, credential, responseEncryption ->
mutex.withLock(this) {

if (data.containsKey(transactionId)) {
require(data[transactionId] == null) { "Oops!! $transactionId already exists" }
}
data[transactionId] = credential
data[transactionId] = credential to responseEncryption

log.info("Stored $transactionId -> $credential ")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,5 @@ sealed interface CredentialResponse<out T> {
* The issuance of the requested Credential has been deferred.
* The deferred transaction can be identified by [transactionId].
*/
data class Deferred(
val transactionId: TransactionId,
) : CredentialResponse<Nothing>
data class Deferred(val transactionId: TransactionId) : CredentialResponse<Nothing>
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,46 @@
package eu.europa.ec.eudi.pidissuer.port.input

import arrow.core.raise.Raise
import com.nimbusds.jose.util.JSONObjectUtils
import com.nimbusds.jwt.JWTClaimsSet
import eu.europa.ec.eudi.pidissuer.domain.RequestedResponseEncryption
import eu.europa.ec.eudi.pidissuer.domain.TransactionId
import eu.europa.ec.eudi.pidissuer.port.out.jose.EncryptCredentialResponse
import eu.europa.ec.eudi.pidissuer.port.out.persistence.LoadDeferredCredentialByTransactionId
import eu.europa.ec.eudi.pidissuer.port.out.persistence.LoadDeferredCredentialResult
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.Required
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import org.slf4j.LoggerFactory

@Serializable
data class DeferredCredentialRequestTO(
@Required @SerialName("transaction_id") val transactionId: String,
)

@Serializable
data class CredentialTO(
@Required val credential: JsonElement,
@SerialName("notification_id") val notificationId: String? = null,
)
sealed interface DeferredCredentialSuccessResponse {

/**
* Deferred response is plain, no encryption
*/
@Serializable
data class PlainTO(
@Required val credential: JsonElement,
@SerialName("notification_id") val notificationId: String? = null,
) : DeferredCredentialSuccessResponse

/**
* Deferred response is encrypted.
*/
data class EncryptedJwtIssued(
val jwt: String,
) : DeferredCredentialSuccessResponse
}

@Serializable
data class GetDeferredCredentialErrorTO(val error: String) {
Expand All @@ -48,21 +68,43 @@ data class GetDeferredCredentialErrorTO(val error: String) {
/**
* Usecase for retrieving/polling a deferred credential
*/
class GetDeferredCredential(val loadDeferredCredentialByTransactionId: LoadDeferredCredentialByTransactionId) {
class GetDeferredCredential(
val loadDeferredCredentialByTransactionId: LoadDeferredCredentialByTransactionId,
val encryptCredentialResponse: EncryptCredentialResponse,
) {

private val log = LoggerFactory.getLogger(GetDeferredCredential::class.java)

context (Raise<GetDeferredCredentialErrorTO>)
suspend operator fun invoke(requestTO: DeferredCredentialRequestTO): CredentialTO = coroutineScope {
suspend operator fun invoke(requestTO: DeferredCredentialRequestTO): DeferredCredentialSuccessResponse = coroutineScope {
val transactionId = TransactionId(requestTO.transactionId)
log.info("GetDeferredCredential for $transactionId ...")
loadDeferredCredentialByTransactionId(transactionId).toTo()
}
}

context (Raise<GetDeferredCredentialErrorTO>)
private fun LoadDeferredCredentialResult.toTo(): CredentialTO = when (this) {
is LoadDeferredCredentialResult.IssuancePending -> raise(GetDeferredCredentialErrorTO.IssuancePending)
is LoadDeferredCredentialResult.InvalidTransactionId -> raise(GetDeferredCredentialErrorTO.InvalidTransactionId)
is LoadDeferredCredentialResult.Found -> CredentialTO(credential.credential, credential.notificationId?.value)
context (Raise<GetDeferredCredentialErrorTO>)
private fun LoadDeferredCredentialResult.toTo(): DeferredCredentialSuccessResponse = when (this) {
is LoadDeferredCredentialResult.IssuancePending -> raise(GetDeferredCredentialErrorTO.IssuancePending)
is LoadDeferredCredentialResult.InvalidTransactionId -> raise(GetDeferredCredentialErrorTO.InvalidTransactionId)
is LoadDeferredCredentialResult.Found -> when (responseEncryption) {
RequestedResponseEncryption.NotRequired ->
DeferredCredentialSuccessResponse.PlainTO(credential.credential, credential.notificationId?.value)

is RequestedResponseEncryption.Required -> {
val plain = DeferredCredentialSuccessResponse.PlainTO(credential.credential, credential.notificationId?.value)
val encryptedJwt: String = encryptCredentialResponse(responseEncryption) { toJwtClaims(plain) }.getOrThrow()
DeferredCredentialSuccessResponse.EncryptedJwtIssued(encryptedJwt)
}
}
}

private 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)
notificationId?.let { claim("notification_id", it) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package eu.europa.ec.eudi.pidissuer.port.input

import arrow.core.*
import arrow.core.raise.*
import com.nimbusds.jose.util.JSONObjectUtils
import com.nimbusds.jwt.JWTClaimsSet
import eu.europa.ec.eudi.pidissuer.domain.*
import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError.*
import eu.europa.ec.eudi.pidissuer.port.out.IssueSpecificCredential
Expand Down Expand Up @@ -320,7 +322,25 @@ class IssueCredential(
val plain = credential.toTO(newCNonce)
return when (val encryption = request.credentialResponseEncryption) {
RequestedResponseEncryption.NotRequired -> plain
is RequestedResponseEncryption.Required -> encryptCredentialResponse(plain, encryption).getOrThrow()
is RequestedResponseEncryption.Required -> {
val jwt = encryptCredentialResponse(encryption) { toJwtClaims(plain) }.getOrThrow()
IssueCredentialResponse.EncryptedJwtIssued(jwt)
}
}
}

private 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)
}
transactionId?.let { claim("transaction_id", it) }
claim("c_nonce", nonce)
claim("c_nonce_expires_in", nonceExpiresIn)
notificationId?.let { claim("notification_id", it) }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ private class DeferredIssuer(
require(credentialResponse is CredentialResponse.Issued<JsonElement>) { "Actual issuer should return issued credentials" }

val transactionId = generateTransactionId()
storeDeferredCredential(transactionId, credentialResponse)
storeDeferredCredential.invoke(transactionId, credentialResponse, request.credentialResponseEncryption)
return CredentialResponse.Deferred(transactionId).also {
log.info("Repackaged $credentialResponse as $it")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
*/
package eu.europa.ec.eudi.pidissuer.port.out.jose

import com.nimbusds.jwt.JWTClaimsSet
import eu.europa.ec.eudi.pidissuer.domain.RequestedResponseEncryption
import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialResponse

fun interface EncryptCredentialResponse {

operator fun invoke(
response: IssueCredentialResponse.PlainTO,
parameters: RequestedResponseEncryption.Required,
): Result<IssueCredentialResponse.EncryptedJwtIssued>
responseAsJwtClaims: JWTClaimsSet.Builder.() -> Unit,
): Result<String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
package eu.europa.ec.eudi.pidissuer.port.out.persistence

import eu.europa.ec.eudi.pidissuer.domain.CredentialResponse
import eu.europa.ec.eudi.pidissuer.domain.RequestedResponseEncryption
import eu.europa.ec.eudi.pidissuer.domain.TransactionId
import kotlinx.serialization.json.JsonElement

sealed interface LoadDeferredCredentialResult {
data class Found(val credential: CredentialResponse.Issued<JsonElement>) : LoadDeferredCredentialResult
data class Found(
val credential: CredentialResponse.Issued<JsonElement>,
val responseEncryption: RequestedResponseEncryption,
) : LoadDeferredCredentialResult
data object InvalidTransactionId : LoadDeferredCredentialResult
data object IssuancePending : LoadDeferredCredentialResult
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
package eu.europa.ec.eudi.pidissuer.port.out.persistence

import eu.europa.ec.eudi.pidissuer.domain.CredentialResponse
import eu.europa.ec.eudi.pidissuer.domain.RequestedResponseEncryption
import eu.europa.ec.eudi.pidissuer.domain.TransactionId
import kotlinx.serialization.json.JsonElement

fun interface StoreDeferredCredential {
suspend operator fun invoke(
transactionId: TransactionId,
credential: CredentialResponse.Issued<JsonElement>,
credentialResponseEncryption: RequestedResponseEncryption,
)
}
2 changes: 2 additions & 0 deletions src/main/resources/application-prod.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ issuer.mdl.mso_mdoc.encoderUrl=https://preprod.issuer.eudiw.dev/formatter/cbor

spring.security.oauth2.resourceserver.opaquetoken.client-id=pid-issuer-srv
spring.security.oauth2.resourceserver.opaquetoken.client-secret=zIKAV9DIIIaJCzHCVBPlySgU8KgY68U2

issuer.keycloak.server-url=https://dev.auth.eudiw.dev
Loading

0 comments on commit a82c3c3

Please sign in to comment.