Skip to content

Commit

Permalink
Addressed PR comments
Browse files Browse the repository at this point in the history
  • Loading branch information
vafeini committed May 30, 2024
1 parent a82c3c3 commit 5f5120d
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 76 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
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,91 @@ 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.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> = 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +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, Pair<CredentialResponse.Issued<JsonElement>, RequestedResponseEncryption>?> =
mutableMapOf(),
private val data: MutableMap<TransactionId, DeferredState> = mutableMapOf(),
) {

private val mutex = Mutex()
Expand All @@ -38,9 +46,11 @@ class InMemoryDeferredCredentialRepository(
LoadDeferredCredentialByTransactionId { transactionId ->
mutex.withLock(this) {
if (data.containsKey(transactionId)) {
data[transactionId]
?.let { (credential, encryption) -> LoadDeferredCredentialResult.Found(credential, encryption) }
?: LoadDeferredCredentialResult.IssuancePending
val deferredPersist = data[transactionId]
if (deferredPersist?.issued != null) {
LoadDeferredCredentialResult.Found(deferredPersist.issued, deferredPersist.responseEncryption)
} else
LoadDeferredCredentialResult.IssuancePending
} else LoadDeferredCredentialResult.InvalidTransactionId
}
}
Expand All @@ -52,7 +62,7 @@ class InMemoryDeferredCredentialRepository(
if (data.containsKey(transactionId)) {
require(data[transactionId] == null) { "Oops!! $transactionId already exists" }
}
data[transactionId] = credential to responseEncryption
data[transactionId] = DeferredState(responseEncryption, credential)

log.info("Stored $transactionId -> $credential ")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,16 @@
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.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
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
Expand Down Expand Up @@ -70,7 +65,7 @@ data class GetDeferredCredentialErrorTO(val error: String) {
*/
class GetDeferredCredential(
val loadDeferredCredentialByTransactionId: LoadDeferredCredentialByTransactionId,
val encryptCredentialResponse: EncryptCredentialResponse,
val encryptCredentialResponse: EncryptDeferredResponse,
) {

private val log = LoggerFactory.getLogger(GetDeferredCredential::class.java)
Expand All @@ -89,22 +84,10 @@ class GetDeferredCredential(
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)
encryptCredentialResponse(plain, responseEncryption).getOrThrow()
}
}
}

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,8 +17,6 @@ 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 @@ -322,25 +320,7 @@ class IssueCredential(
val plain = credential.toTO(newCNonce)
return when (val encryption = request.credentialResponseEncryption) {
RequestedResponseEncryption.NotRequired -> plain
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) }
is RequestedResponseEncryption.Required -> encryptCredentialResponse(plain, encryption).getOrThrow()
}
}

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,
responseAsJwtClaims: JWTClaimsSet.Builder.() -> Unit,
): Result<String>
): Result<IssueCredentialResponse.EncryptedJwtIssued>
}
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>
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import kotlinx.serialization.json.JsonElement
fun interface StoreDeferredCredential {
suspend operator fun invoke(
transactionId: TransactionId,
credential: CredentialResponse.Issued<JsonElement>,
credential: CredentialResponse.Issued<JsonElement>?,
credentialResponseEncryption: RequestedResponseEncryption,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import com.nimbusds.jose.jwk.source.ImmutableJWKSet
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier
import com.nimbusds.jose.proc.JWEDecryptionKeySelector
import com.nimbusds.jose.proc.SecurityContext
import com.nimbusds.jose.util.JSONObjectUtils
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier
import com.nimbusds.jwt.proc.DefaultJWTProcessor
Expand All @@ -51,7 +50,7 @@ internal class EncryptCredentialResponseWithNimbusTest {

private val issuer = CredentialIssuerId.unsafe("https://eudi.ec.europa.eu/issuer")
private val clock = Clock.systemDefaultZone()
private val encrypter = EncryptCredentialResponseWithNimbus(issuer, clock)
private val encrypter = EncryptCredentialResponseNimbus(issuer, clock)

@Test
internal fun `encrypt response with RSA`() = runTest {
Expand Down Expand Up @@ -96,7 +95,7 @@ internal class EncryptCredentialResponseWithNimbusTest {
parameters: RequestedResponseEncryption.Required,
decryptionKey: JWK,
) {
val encryptedJwt = encrypter(parameters) { toJwtClaims(unencrypted) }.getOrElse { fail(it.message, it) }
val encrypted = encrypter(unencrypted, parameters).getOrElse { fail(it.message, it) }

val processor = DefaultJWTProcessor<SecurityContext>().apply {
jweTypeVerifier = DefaultJOSEObjectTypeVerifier.JWT
Expand All @@ -119,7 +118,7 @@ internal class EncryptCredentialResponseWithNimbusTest {
)
}

val claims = runCatching { processor.process(encryptedJwt, null) }.getOrElse { fail(it.message, it) }
val claims = runCatching { processor.process(encrypted.jwt, null) }.getOrElse { fail(it.message, it) }
val credential = claims.getClaim("credential")
?.let {
when (it) {
Expand All @@ -133,19 +132,4 @@ internal class EncryptCredentialResponseWithNimbusTest {
}
assertEquals(unencrypted.credential, credential)
}

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) }
}
}
}

0 comments on commit 5f5120d

Please sign in to comment.