Skip to content

Commit

Permalink
Response encryption for deferred credential issuance. (#173)
Browse files Browse the repository at this point in the history
Co-authored-by: Vafeiadis Nikos <nvafeiadis@netcompany.com>
  • Loading branch information
vafeini and vafeini authored May 30, 2024
1 parent 8461a32 commit c50cb77
Show file tree
Hide file tree
Showing 12 changed files with 194 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ import eu.europa.ec.eudi.pidissuer.adapter.out.IssuerSigningKey
import eu.europa.ec.eudi.pidissuer.adapter.out.credential.CredentialRequestFactory
import eu.europa.ec.eudi.pidissuer.adapter.out.credential.DefaultResolveCredentialRequestByCredentialIdentifier
import eu.europa.ec.eudi.pidissuer.adapter.out.jose.DefaultExtractJwkFromCredentialKey
import eu.europa.ec.eudi.pidissuer.adapter.out.jose.EncryptCredentialResponseWithNimbus
import eu.europa.ec.eudi.pidissuer.adapter.out.jose.EncryptCredentialResponseNimbus
import eu.europa.ec.eudi.pidissuer.adapter.out.jose.EncryptDeferredResponseNimbus
import eu.europa.ec.eudi.pidissuer.adapter.out.mdl.*
import eu.europa.ec.eudi.pidissuer.adapter.out.persistence.InMemoryCNonceRepository
import eu.europa.ec.eudi.pidissuer.adapter.out.persistence.InMemoryDeferredCredentialRepository
Expand Down Expand Up @@ -351,7 +352,10 @@ fun beans(clock: Clock) = beans {
// Encryption of credential response
//
bean(isLazyInit = true) {
EncryptCredentialResponseWithNimbus(ref<CredentialIssuerMetaData>().id, clock)
EncryptDeferredResponseNimbus(ref<CredentialIssuerMetaData>().id, clock)
}
bean(isLazyInit = true) {
EncryptCredentialResponseNimbus(ref<CredentialIssuerMetaData>().id, clock)
}
//
// CNonce
Expand All @@ -376,7 +380,7 @@ fun beans(clock: Clock) = beans {
//
// Deferred Credentials
//
with(InMemoryDeferredCredentialRepository(mutableMapOf(TransactionId("foo") to null))) {
with(InMemoryDeferredCredentialRepository(mutableMapOf())) {
bean { GenerateTransactionId.Random }
bean { storeDeferredCredential }
bean { loadDeferredCredentialByTransactionId }
Expand Down Expand Up @@ -466,7 +470,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 @@ -27,34 +27,95 @@ 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.DeferredCredentialSuccessResponse
import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialResponse
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.JsonPrimitive
import java.time.Clock
import java.time.Instant
import java.util.*

/**
* Implementation of [EncryptDeferredResponse] using Nimbus.
*/
class EncryptDeferredResponseNimbus(
issuer: CredentialIssuerId,
clock: Clock,
) : EncryptDeferredResponse {

private val encryptResponse = EncryptResponse(issuer, clock)

override fun invoke(
response: DeferredCredentialSuccessResponse.PlainTO,
parameters: RequestedResponseEncryption.Required,
): 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)
notificationId?.let { claim("notification_id", it) }
}
}

val jwt = encryptResponse(parameters) { toJwtClaims(response) }.getOrThrow()
DeferredCredentialSuccessResponse.EncryptedJwtIssued(jwt)
}
}

/**
* Implementation of [EncryptCredentialResponse] using Nimbus.
*/
class EncryptCredentialResponseWithNimbus(
private val issuer: CredentialIssuerId,
private val clock: Clock,
class EncryptCredentialResponseNimbus(
issuer: CredentialIssuerId,
clock: Clock,
) : EncryptCredentialResponse {

private val encryptResponse = EncryptResponse(issuer, clock)

override fun invoke(
response: IssueCredentialResponse.PlainTO,
parameters: RequestedResponseEncryption.Required,
): Result<IssueCredentialResponse.EncryptedJwtIssued> = runCatching {
): 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)
}
transactionId?.let { claim("transaction_id", it) }
claim("c_nonce", nonce)
claim("c_nonce_expires_in", nonceExpiresIn)
notificationId?.let { claim("notification_id", it) }
}
}

val jwt = encryptResponse(parameters) { toJwtClaims(response) }.getOrThrow()
IssueCredentialResponse.EncryptedJwtIssued(jwt)
}
}

private class EncryptResponse(
private val issuer: CredentialIssuerId,
private val clock: Clock,
) {

operator fun invoke(
parameters: RequestedResponseEncryption.Required,
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 +125,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 @@ -25,9 +26,18 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.JsonElement
import org.slf4j.LoggerFactory

/**
* Represents the state of the deferred issuance. Holds the response encryption as specified in initial request
* and the issued credential. If issuance is still pending [issued] is null.
*/
data class DeferredState(
val responseEncryption: RequestedResponseEncryption,
val issued: CredentialResponse.Issued<JsonElement>?,
)

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

private val mutex = Mutex()
Expand All @@ -36,21 +46,23 @@ class InMemoryDeferredCredentialRepository(
LoadDeferredCredentialByTransactionId { transactionId ->
mutex.withLock(this) {
if (data.containsKey(transactionId)) {
data[transactionId]
?.let { LoadDeferredCredentialResult.Found(it) }
?: LoadDeferredCredentialResult.IssuancePending
val deferredPersist = data[transactionId]
if (deferredPersist?.issued != null) {
LoadDeferredCredentialResult.Found(deferredPersist.issued, deferredPersist.responseEncryption)
} else
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] = DeferredState(responseEncryption, credential)

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,7 +16,9 @@
package eu.europa.ec.eudi.pidissuer.port.input

import arrow.core.raise.Raise
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.EncryptDeferredResponse
import eu.europa.ec.eudi.pidissuer.port.out.persistence.LoadDeferredCredentialByTransactionId
import eu.europa.ec.eudi.pidissuer.port.out.persistence.LoadDeferredCredentialResult
import kotlinx.coroutines.coroutineScope
Expand All @@ -31,11 +33,24 @@ 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 +63,31 @@ 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: EncryptDeferredResponse,
) {

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)
encryptCredentialResponse(plain, responseEncryption).getOrThrow()
}
}
}
}
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
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 European Commission
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.europa.ec.eudi.pidissuer.port.out.jose

import eu.europa.ec.eudi.pidissuer.domain.RequestedResponseEncryption
import eu.europa.ec.eudi.pidissuer.port.input.DeferredCredentialSuccessResponse

fun interface EncryptDeferredResponse {

operator fun invoke(
response: DeferredCredentialSuccessResponse.PlainTO,
parameters: RequestedResponseEncryption.Required,
): Result<DeferredCredentialSuccessResponse.EncryptedJwtIssued>
}
Loading

0 comments on commit c50cb77

Please sign in to comment.