diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt index 58d4aba..e7be26a 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt @@ -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 @@ -351,7 +352,10 @@ fun beans(clock: Clock) = beans { // Encryption of credential response // bean(isLazyInit = true) { - EncryptCredentialResponseWithNimbus(ref().id, clock) + EncryptDeferredResponseNimbus(ref().id, clock) + } + bean(isLazyInit = true) { + EncryptCredentialResponseNimbus(ref().id, clock) } // // CNonce @@ -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 } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptCredentialResponseWithNimbus.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptResponseWithNimbus.kt similarity index 50% rename from src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptCredentialResponseWithNimbus.kt rename to src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptResponseWithNimbus.kt index f111f12..469c326 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptCredentialResponseWithNimbus.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptResponseWithNimbus.kt @@ -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 = 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 = 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 = runCatching { diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/persistence/InMemoryDeferredCredentialRepository.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/persistence/InMemoryDeferredCredentialRepository.kt index eab766a..cd454ff 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/persistence/InMemoryDeferredCredentialRepository.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/persistence/InMemoryDeferredCredentialRepository.kt @@ -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?, +) + private val log = LoggerFactory.getLogger(InMemoryDeferredCredentialRepository::class.java) class InMemoryDeferredCredentialRepository( - private val data: MutableMap, RequestedResponseEncryption>?> = - mutableMapOf(), + private val data: MutableMap = mutableMapOf(), ) { private val mutex = Mutex() @@ -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 } } @@ -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 ") } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/GetDeferredCredential.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/GetDeferredCredential.kt index 99f8767..de21fad 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/GetDeferredCredential.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/GetDeferredCredential.kt @@ -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 @@ -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) @@ -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) } - } - } } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/IssueCredential.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/IssueCredential.kt index c3561d8..30791d8 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/IssueCredential.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/IssueCredential.kt @@ -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 @@ -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() } } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/EncryptCredentialResponse.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/EncryptCredentialResponse.kt index b25a45b..dc0fcb5 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/EncryptCredentialResponse.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/EncryptCredentialResponse.kt @@ -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 + ): Result } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/EncryptDeferredResponse.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/EncryptDeferredResponse.kt new file mode 100644 index 0000000..4d59c8d --- /dev/null +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/jose/EncryptDeferredResponse.kt @@ -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 +} diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/persistence/StoreDeferredCredential.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/persistence/StoreDeferredCredential.kt index 4f4b98e..eb699c9 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/persistence/StoreDeferredCredential.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/persistence/StoreDeferredCredential.kt @@ -23,7 +23,7 @@ import kotlinx.serialization.json.JsonElement fun interface StoreDeferredCredential { suspend operator fun invoke( transactionId: TransactionId, - credential: CredentialResponse.Issued, + credential: CredentialResponse.Issued?, credentialResponseEncryption: RequestedResponseEncryption, ) } diff --git a/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptCredentialResponseWithNimbusTest.kt b/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptCredentialResponseWithNimbusTest.kt index 85e1822..62924e0 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptCredentialResponseWithNimbusTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/jose/EncryptCredentialResponseWithNimbusTest.kt @@ -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 @@ -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 { @@ -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().apply { jweTypeVerifier = DefaultJOSEObjectTypeVerifier.JWT @@ -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) { @@ -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) } - } - } }