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 e97691c8..e7be26ae 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 } @@ -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) } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/WalletApi.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/WalletApi.kt index 450ea665..5a15e960 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/WalletApi.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/WalletApi.kt @@ -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 @@ -95,10 +96,23 @@ class WalletApi( private suspend fun handleGetDeferredCredential(req: ServerRequest): ServerResponse = coroutineScope { val requestTO = req.awaitBody() - 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 { 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 53% 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 38f410e1..469c3266 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 @@ -27,8 +27,10 @@ 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 @@ -36,25 +38,84 @@ 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 = runCatching { + ): 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 { 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() = @@ -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) { 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 d6fef89c..cd454ff5 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 @@ -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 @@ -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?, +) + private val log = LoggerFactory.getLogger(InMemoryDeferredCredentialRepository::class.java) class InMemoryDeferredCredentialRepository( - private val data: MutableMap?> = mutableMapOf(), + private val data: MutableMap = mutableMapOf(), ) { private val mutex = Mutex() @@ -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 ") } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialResponse.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialResponse.kt index 4281086e..ab7ed1f7 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialResponse.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialResponse.kt @@ -41,7 +41,5 @@ sealed interface CredentialResponse { * The issuance of the requested Credential has been deferred. * The deferred transaction can be identified by [transactionId]. */ - data class Deferred( - val transactionId: TransactionId, - ) : CredentialResponse + data class Deferred(val transactionId: TransactionId) : CredentialResponse } 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 a0bbf18f..de21fad3 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,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 @@ -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) { @@ -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) - 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) -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) + 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() + } + } + } } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/IssueSpecificCredential.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/IssueSpecificCredential.kt index a19b460e..81a796d3 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/IssueSpecificCredential.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/IssueSpecificCredential.kt @@ -64,7 +64,7 @@ private class DeferredIssuer( require(credentialResponse is CredentialResponse.Issued) { "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") } 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 00000000..4d59c8dd --- /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/LoadDeferredCredentialByTransactionId.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/persistence/LoadDeferredCredentialByTransactionId.kt index 43932812..a147f0bc 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/persistence/LoadDeferredCredentialByTransactionId.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/persistence/LoadDeferredCredentialByTransactionId.kt @@ -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) : LoadDeferredCredentialResult + data class Found( + val credential: CredentialResponse.Issued, + val responseEncryption: RequestedResponseEncryption, + ) : LoadDeferredCredentialResult data object InvalidTransactionId : LoadDeferredCredentialResult data object IssuancePending : LoadDeferredCredentialResult } 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 d982a4bf..eb699c9e 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 @@ -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, + credential: CredentialResponse.Issued?, + credentialResponseEncryption: RequestedResponseEncryption, ) } diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index bb63b7f3..fbcf1e50 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -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 \ No newline at end of file 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 2a831b6d..62924e0e 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 @@ -50,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 {